Compare commits
292 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85c81d8c0e |
||
|
|
6d51d7d08c |
||
|
|
93d9b73867 |
||
|
|
ac210fb634 |
||
|
|
df828e145d |
||
|
|
cedd39a65f |
||
|
|
12b98b2338 |
||
|
|
8a7f05ac2a |
||
|
|
0fca6e961d |
||
|
|
32de202845 |
||
|
|
6cff2d8b05 |
||
|
|
4c06cb3204 | ||
|
|
9a627985d0 | ||
|
|
24a037ad19 |
||
|
|
40daae64e3 |
||
|
|
d3fc084c42 |
||
|
|
bcab551f05 |
||
|
|
04779f8502 |
||
|
|
bd18fc980c |
||
|
|
5a2ea1eef9 | ||
|
|
d2f88b4648 |
||
|
|
d8fe79fa40 |
||
|
|
f0dd16ebfe |
||
|
|
0813a7adf9 |
||
|
|
7a36b19729 |
||
|
|
98a2d1ce50 |
||
|
|
22e2e226cb |
||
|
|
f1cd9e606d |
||
|
|
80f500874b |
||
|
|
5f21dcdc56 | ||
|
|
02038d40c1 | ||
|
|
6b753009ac | ||
|
|
d7da2a5bd5 | ||
|
|
c00d7dc340 | ||
|
|
7f061de317 | ||
|
|
0a7136b8fc |
||
|
|
7440393d3a |
||
|
|
f6bda9ae89 | ||
|
|
1911a1cfe0 | ||
|
|
db4acbf7d6 | ||
|
|
6a0c2871ad | ||
|
|
9cf08c4f9c | ||
|
|
983cc8882b | ||
|
|
8dfb8727cd | ||
|
|
9396b3e69d | ||
|
|
e6877a5198 | ||
|
|
7a7cdd8e76 | ||
|
|
023f5d2463 | ||
|
|
65e305e6e9 | ||
|
|
05766db4b6 | ||
|
|
e422087217 | ||
|
|
d08a0356d2 | ||
|
|
9c8a34e567 | ||
|
|
1bbd9c694e | ||
|
|
b99a5430f0 | ||
|
|
fc062c2d48 | ||
|
|
5e9709bc5b | ||
|
|
c091af1622 | ||
|
|
33af725e7d | ||
|
|
cd3e7cc1c8 | ||
|
|
ad3c0a63ee | ||
|
|
20fe35355c | ||
|
|
50bed1f6e9 | ||
|
|
1ea7208b4d | ||
|
|
4186a4b37b | ||
|
|
0118aea506 | ||
|
|
2936245d39 | ||
|
|
fc9f9312ef | ||
|
|
c8c7b13d2f | ||
|
|
a2c2aac639 | ||
|
|
18b5a12d1e | ||
|
|
a90efe752d | ||
|
|
068c30c97d | ||
|
|
16008dbe52 | ||
|
|
b16112613a | ||
|
|
1ccd8b1c16 | ||
|
|
3a2effdf99 | ||
|
|
fc0ca22785 | ||
|
|
a07f1c7b82 | ||
|
|
bd69de9f14 | ||
|
|
1084849500 | ||
|
|
b05c037bb0 | ||
|
|
b3a5ed9c54 | ||
|
|
015590e806 | ||
|
|
6ab7c9455d | ||
|
|
3b3aa6347d | ||
|
|
20fe1bcbee | ||
|
|
fbfeced5b1 | ||
|
|
52026d1687 | ||
|
|
451ee92dd0 | ||
|
|
330a7351f9 | ||
|
|
d8df257dd2 | ||
|
|
092395ed5c | ||
|
|
78cad87452 | ||
|
|
9882d1aec2 | ||
|
|
9068686614 | ||
|
|
1e3a1e4745 | ||
|
|
87edc4cdb9 | ||
|
|
75739f8170 | ||
|
|
dd0ea311a3 | ||
|
|
5576c4809d | ||
|
|
b36380a48e | ||
|
|
db9325b3bb | ||
|
|
b5fc4de010 | ||
|
|
7aace27355 | ||
|
|
271fdd9bde | ||
|
|
4428c4ed95 | ||
|
|
135044d8d7 | ||
|
|
73c94c969c |
||
|
|
387c8b79fb |
||
|
|
be759d9b42 | ||
|
|
c9c30eaf45 | ||
|
|
22f24a1a8a | ||
|
|
1ad7567b28 | ||
|
|
28bccf9223 | ||
|
|
b3100d5684 | ||
|
|
d07fc9a8af | ||
|
|
4571ad0dd5 | ||
|
|
b8c9ff45fe | ||
|
|
9ad41a6066 | ||
|
|
7425568122 | ||
|
|
84720812ea | ||
|
|
6c2513a48c | ||
|
|
22a20b3246 | ||
|
|
bf28a44439 | ||
|
|
fb604321f3 | ||
|
|
bcc45feea3 | ||
|
|
e112f15092 | ||
|
|
a0665bc544 | ||
|
|
0ddf6098f7 | ||
|
|
4be45499f7 | ||
|
|
6ea55856b5 | ||
|
|
6bd32a0e88 | ||
|
|
9a8b020269 | ||
|
|
5bc983e0bf | ||
|
|
8e8ada7cd2 | ||
|
|
bb1732c834 | ||
|
|
57a6fca9d1 | ||
|
|
312eb5180a | ||
|
|
45951c96a4 | ||
|
|
55b551678a | ||
|
|
34e2b8b7b4 | ||
|
|
a6a1d7e150 | ||
|
|
79d0b21cb3 | ||
|
|
c324793f62 | ||
|
|
741ff50cae | ||
|
|
2c3e8be9b4 | ||
|
|
539f6d7244 | ||
|
|
e70924c88e | ||
|
|
db0bba2c3f | ||
|
|
775a4a1289 | ||
|
|
f663d2f21b | ||
|
|
4032bed306 | ||
|
|
cb2dd6331d | ||
|
|
2482c772f8 | ||
|
|
9fce711bd6 | ||
|
|
56a50679de | ||
|
|
46b1259498 | ||
|
|
bfb9985754 | ||
|
|
7c08c6bb76 | ||
|
|
bb846644b5 | ||
|
|
f9525dc646 | ||
|
|
e94071b077 | ||
|
|
2ced690387 | ||
|
|
e4d8cb4c20 | ||
|
|
b3c83ed119 | ||
|
|
507094bdfd | ||
|
|
91d7ba25ea | ||
|
|
5838271f8b | ||
|
|
1e378b2ac2 | ||
|
|
6c152ed516 | ||
|
|
ec4a413a7b | ||
|
|
dc88eeaae3 | ||
|
|
920bd2db9d | ||
|
|
dc7409e52e | ||
|
|
907c372c27 | ||
|
|
7779560b6b | ||
|
|
a93e739081 | ||
|
|
5f7f33bb02 | ||
|
|
43ee890356 | ||
|
|
d538b7bc93 | ||
|
|
ea7eee7564 | ||
|
|
8e777a0c19 | ||
|
|
e739909466 | ||
|
|
8eef2074b4 | ||
|
|
c681f0055f | ||
|
|
4199c42e3b | ||
|
|
0ad9d68c81 | ||
|
|
ba88fb9a0b | ||
|
|
c987323dba | ||
|
|
fe7d72fa4e | ||
|
|
418c027f6c | ||
|
|
8a1424669f | ||
|
|
09a5e1c34e | ||
|
|
4bfe66edd6 | ||
|
|
74c07f0634 | ||
|
|
f89da765fa | ||
|
|
64bc900456 | ||
|
|
cd74cd9d4e | ||
|
|
9aa3ba545d | ||
|
|
79270c3423 | ||
|
|
2d48975e97 | ||
|
|
c8030bc185 | ||
|
|
051e9985b1 | ||
|
|
bd2d8e7d3f | ||
|
|
8a2b893d5e | ||
|
|
67ee246f40 | ||
|
|
994a51a1f0 | ||
|
|
8dbc1da708 | ||
|
|
a535ad709b | ||
|
|
d9648be60c | ||
|
|
ad0a3ffda9 | ||
|
|
e58cbb78d2 | ||
|
|
c86916e37d | ||
|
|
4801b8d8c1 | ||
|
|
d1e0e1367b | ||
|
|
b189b24a4f | ||
|
|
2c05c772ca | ||
|
|
972fcb0950 | ||
|
|
7b246d0d40 | ||
|
|
e68d92d22a | ||
|
|
28c4f4b76a | ||
|
|
b3fba429ad | ||
|
|
08538f7f26 | ||
|
|
e33b9dc09a | ||
|
|
472f1deb58 | ||
|
|
14660f9d68 | ||
|
|
45931c5e4a | ||
|
|
d5596c8ff5 | ||
|
|
3277f19503 | ||
|
|
f9460e6e05 | ||
|
|
dc5ba1f646 | ||
|
|
f17e37ac95 | ||
|
|
e49fa7edf7 | ||
|
|
18d0187228 | ||
|
|
fca6b2a6ca | ||
|
|
ed3d467f31 | ||
|
|
22f458724f | ||
|
|
8a639f3121 | ||
|
|
06f5283709 | ||
|
|
a9b5216a42 | ||
|
|
c895d18ccb | ||
|
|
d6c7be7106 | ||
|
|
76a2031c37 | ||
|
|
68134abe3f | ||
|
|
084a1988c8 | ||
|
|
9160776999 | ||
|
|
20bf4a31d5 | ||
|
|
28f3e61b2e | ||
|
|
1f0c5d98dd | ||
|
|
b44f699dd0 | ||
|
|
521ef9baec | ||
|
|
63c7cbc43b | ||
|
|
21c911ffd3 | ||
|
|
2d1e656061 | ||
|
|
8666d8d6d6 | ||
|
|
23f00055a9 | ||
|
|
cf9b22ae62 | ||
|
|
30491d401a | ||
|
|
b1e60fbd21 | ||
|
|
a9c4b7ab67 | ||
|
|
6aa87e4b81 | ||
|
|
00354cfc18 | ||
|
|
479db8c2cc | ||
|
|
459d696128 | ||
|
|
3238422412 | ||
|
|
7d3224183a | ||
|
|
27cacbd271 | ||
|
|
037ddef91d | ||
|
|
371ae67c64 | ||
|
|
e21b4fa6a1 | ||
|
|
2c0dad5094 | ||
|
|
be4159fc11 | ||
|
|
549a643fb9 | ||
|
|
c74b90876d | ||
|
|
f0e2d41fde | ||
|
|
0255f58964 | ||
|
|
5f083a7d01 | ||
|
|
686ad23f2f | ||
|
|
5bc7d4e336 | ||
|
|
d9bb8d99e8 | ||
|
|
5bd43f13f9 | ||
|
|
cfed5e05ec | ||
|
|
546162c388 | ||
|
|
3f14cdcf0d | ||
|
|
a9b12a231f | ||
|
|
5d71662617 | ||
|
|
199408f160 | ||
|
|
74c5fb48ff | ||
|
|
be0f9c9e08 | ||
|
|
21bb6a0311 | ||
|
|
a799413f7f |
95 changed files with 19646 additions and 1851 deletions
31
.github/workflows/ci.yml
vendored
Normal file
31
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the master branch
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
container: nimlang/nim
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
# Runs a single command using the runners shell
|
||||
- name: Tests
|
||||
run: |
|
||||
nimble develop -y
|
||||
nim c -r tests/tester.nim
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1 +1,7 @@
|
|||
nimcache/
|
||||
/tests/tester
|
||||
karax/tools/karun
|
||||
*.code-workspace
|
||||
*.exe
|
||||
app.js
|
||||
app.html
|
||||
|
|
|
|||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Xored Software, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
BIN
docs/benchmark.png
Normal file
BIN
docs/benchmark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
58
docs/docs.rst
Normal file
58
docs/docs.rst
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
Virtual DOM
|
||||
===========
|
||||
|
||||
The virtual dom is in the ``vdom`` module.
|
||||
|
||||
The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes
|
||||
and some extensions that allow for an efficient component system.
|
||||
|
||||
These extensions are:
|
||||
|
||||
``VNodeKind.int``
|
||||
The node has a single integer field.
|
||||
|
||||
``VNodeKind.bool``
|
||||
The node has a single boolean field.
|
||||
|
||||
``VNodeKind.vthunk``
|
||||
The node is a `virtual thunk`:idx:. This means there is a
|
||||
function attached to it that produces the ``VNode`` structure
|
||||
on demand.
|
||||
|
||||
``VNodeKind.dthunk``
|
||||
The node is a `DOM thunk`:idx:. This means there is a
|
||||
function attached to it that produces the ``Node`` DOM structure
|
||||
on demand.
|
||||
|
||||
For convenience of the resulting Nim DSL these tags have enum names
|
||||
that differ from their HTML equivalent:
|
||||
|
||||
================= =======================================================
|
||||
Enum value HTML
|
||||
================= =======================================================
|
||||
``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim)
|
||||
``italic`` ``i``
|
||||
``bold`` ``b``
|
||||
``strikethrough`` ``s``
|
||||
``underlined`` ``u``
|
||||
================= =======================================================
|
||||
|
||||
|
||||
A virtual dom node also has a special field set called ``key``, an integer
|
||||
that can be used as a data model specific key/id. It can be accessed in event
|
||||
handlers to change the data model. See the todoapp for an example of how to
|
||||
use it.
|
||||
|
||||
**Note**: This is not a hint for the DOM diff algorithm, multipe nodes can
|
||||
all have the same key value.
|
||||
|
||||
|
||||
Event system
|
||||
============
|
||||
|
||||
Karax does not abstract over the event system the DOM offers much: The same
|
||||
``Event`` structure is used. Every callback has the
|
||||
signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``.
|
||||
|
||||
|
||||
15
examples/button.nim
Normal file
15
examples/button.nim
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
include karax / prelude
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button:
|
||||
text "Say hello!"
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
lines.add "Hello simulated universe"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
14
examples/buttonlambda.nim
Normal file
14
examples/buttonlambda.nim
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
include karax / prelude
|
||||
from future import `=>`
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button(onclick = () => lines.add "Hello simulated universe"):
|
||||
text "Say hello!"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
15
examples/carousel/carousel.html
Normal file
15
examples/carousel/carousel.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Carousel app</title>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="carousel.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
69
examples/carousel/carousel.nim
Normal file
69
examples/carousel/carousel.nim
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
## This demo shows how you can develop your own stateful components with Karax.
|
||||
|
||||
import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom
|
||||
|
||||
type
|
||||
Carousel = ref object of VComponent
|
||||
counter: int
|
||||
cntdown: int
|
||||
timer: TimeOut
|
||||
list: seq[cstring]
|
||||
|
||||
const ticksUntilChange = 5
|
||||
|
||||
var
|
||||
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
|
||||
|
||||
proc render(x: VComponent): VNode =
|
||||
let self = Carousel(x)
|
||||
|
||||
proc docount() =
|
||||
dec self.cntdown
|
||||
if self.cntdown == 0:
|
||||
self.counter = (self.counter + 1) mod self.list.len
|
||||
self.cntdown = ticksUntilChange
|
||||
else:
|
||||
self.timer = setTimeout(docount, 30)
|
||||
markDirty(self)
|
||||
redraw()
|
||||
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
if self.timer != nil:
|
||||
clearTimeout(self.timer)
|
||||
self.timer = setTimeout(docount, 30)
|
||||
|
||||
let col =
|
||||
case self.counter
|
||||
of 0: cstring"#4d4d4d"
|
||||
of 1: cstring"#ff00ff"
|
||||
of 2: cstring"#00ffff"
|
||||
of 3: cstring"#ffff00"
|
||||
else: cstring"red"
|
||||
result = buildHtml(tdiv()):
|
||||
text self.list[self.counter]
|
||||
button(onclick = onclick):
|
||||
text "Next"
|
||||
tdiv(style = style(StyleAttr.color, col)):
|
||||
text "This changes its color."
|
||||
if self.cntdown != ticksUntilChange:
|
||||
text &self.cntdown
|
||||
|
||||
proc carousel(): Carousel =
|
||||
result = newComponent(Carousel, render)
|
||||
result.list = images
|
||||
result.cntdown = ticksUntilChange
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(table):
|
||||
tr:
|
||||
td:
|
||||
carousel()
|
||||
td:
|
||||
carousel()
|
||||
tr:
|
||||
td:
|
||||
carousel()
|
||||
td:
|
||||
carousel()
|
||||
|
||||
setRenderer createDom
|
||||
1
examples/carousel/carousel.nims
Normal file
1
examples/carousel/carousel.nims
Normal file
|
|
@ -0,0 +1 @@
|
|||
--path: "../../karax"
|
||||
1
examples/config.nims
Normal file
1
examples/config.nims
Normal file
|
|
@ -0,0 +1 @@
|
|||
--path: ".."
|
||||
17
examples/hellostyle.nim
Normal file
17
examples/hellostyle.nim
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
include karax / prelude
|
||||
import karax / vstyles
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
tdiv(style = style(StyleAttr.color, "red".cstring)):
|
||||
text "red"
|
||||
tdiv(style = style(StyleAttr.color, "blue")):
|
||||
text "blue"
|
||||
# explicit `kstring` required for varargs overload
|
||||
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))):
|
||||
text "italic orange"
|
||||
# can use a string directly
|
||||
tdiv(style = "font-style: oblique; color: pink".toCss):
|
||||
text "oblique pink"
|
||||
|
||||
setRenderer createDom
|
||||
12
examples/hellouniverse.nim
Normal file
12
examples/hellouniverse.nim
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
include karax / prelude
|
||||
import random
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if rand(100) <= 50:
|
||||
text "Hello World!"
|
||||
else:
|
||||
text "Hello Universe"
|
||||
|
||||
randomize()
|
||||
setRenderer createDom
|
||||
7
examples/helloworld.nim
Normal file
7
examples/helloworld.nim
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
include karax / prelude
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
text "Hello World!"
|
||||
|
||||
setRenderer createDom
|
||||
46
examples/login.nim
Normal file
46
examples/login.nim
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
include karax / prelude
|
||||
from sugar import `=>`
|
||||
import karax / errors
|
||||
|
||||
proc loginField(desc, field, class: kstring;
|
||||
validator: proc (field: kstring): proc ()): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
label(`for` = field):
|
||||
text desc
|
||||
input(class = class, id = field, onchange = validator(field))
|
||||
|
||||
# some consts in order to prevent typos:
|
||||
const
|
||||
username = kstring"username"
|
||||
password = kstring"password"
|
||||
|
||||
proc validateNotEmpty(field: kstring): proc () =
|
||||
result = proc () =
|
||||
let x = getVNodeById(field)
|
||||
if x.text == "":
|
||||
errors.setError(field, field & " must not be empty")
|
||||
else:
|
||||
errors.setError(field, "")
|
||||
|
||||
var loggedIn: bool
|
||||
|
||||
proc loginDialog(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if not loggedIn:
|
||||
loginField("Name: ", username, "input", validateNotEmpty)
|
||||
loginField("Password: ", password, "password", validateNotEmpty)
|
||||
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
|
||||
text "Login"
|
||||
p:
|
||||
text errors.getError(username)
|
||||
p:
|
||||
text errors.getError(password)
|
||||
else:
|
||||
p:
|
||||
text "You are now logged in."
|
||||
|
||||
setError username, username & " must not be empty"
|
||||
setError password, password & " must not be empty"
|
||||
|
||||
when not declared(toychat):
|
||||
setRenderer loginDialog
|
||||
42
examples/mediaplayer/mediaplayer.nim
Normal file
42
examples/mediaplayer/mediaplayer.nim
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
import karax / [karax, karaxdsl, vdom, kdom, compact]
|
||||
|
||||
const
|
||||
Play = 0
|
||||
Big = 1
|
||||
Small = 2
|
||||
Normal = 3
|
||||
|
||||
proc paused(n: Node): bool {.importcpp: "#.paused".}
|
||||
proc play(n: Node) {.importcpp.}
|
||||
proc pause(n: Node) {.importcpp.}
|
||||
proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".}
|
||||
|
||||
proc mplayer*(id, resource: cstring): VNode {.compact.} =
|
||||
proc handler(ev: Event; n: VNode) =
|
||||
let myVideo = document.getElementById(id)
|
||||
case n.index
|
||||
of Play:
|
||||
if myVideo.paused:
|
||||
myVideo.play()
|
||||
else:
|
||||
myVideo.pause()
|
||||
of Big: myVideo.width = 560
|
||||
of Small: myVideo.width = 320
|
||||
of Normal: myVideo.width = 420
|
||||
else: discard
|
||||
|
||||
result = buildHtml(tdiv):
|
||||
button(onclick=handler, index=Play):
|
||||
text "Play/Pause"
|
||||
button(onclick=handler, index=Big):
|
||||
text "Big"
|
||||
button(onclick=handler, index=Small):
|
||||
text "Small"
|
||||
button(onclick=handler, index=Normal):
|
||||
text "Normal"
|
||||
br()
|
||||
br()
|
||||
video(id=id, width="420"):
|
||||
source(src=resource, `type`="video/mp4"):
|
||||
text "Your browser does not support HTML5 video."
|
||||
15
examples/mediaplayer/playerapp.html
Normal file
15
examples/mediaplayer/playerapp.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Player app</title>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="playerapp.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
21
examples/mediaplayer/playerapp.nim
Normal file
21
examples/mediaplayer/playerapp.nim
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Example app that shows how to write and embed a custom component.
|
||||
|
||||
include karax/prelude
|
||||
import mediaplayer
|
||||
|
||||
const url = "https://www.w3schools.com/html/mov_bbb.mp4"
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(table):
|
||||
tr:
|
||||
td:
|
||||
mplayer("vid1", url)
|
||||
td:
|
||||
mplayer("vid2", url)
|
||||
tr:
|
||||
td:
|
||||
mplayer("vid3", url)
|
||||
td:
|
||||
mplayer("vid4", url)
|
||||
|
||||
setRenderer createDom
|
||||
1
examples/mediaplayer/playerapp.nims
Normal file
1
examples/mediaplayer/playerapp.nims
Normal file
|
|
@ -0,0 +1 @@
|
|||
--path: "../.."
|
||||
15
examples/scrollapp/scrollapp.html
Normal file
15
examples/scrollapp/scrollapp.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Todo app</title>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="scrollapp.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
25
examples/scrollapp/scrollapp.nim
Normal file
25
examples/scrollapp/scrollapp.nim
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
## Example that shows how to accomplish an "infinitely scrolling" app.
|
||||
|
||||
include karax/prelude
|
||||
import karax / [jstrutils, kdom, vstyles]
|
||||
|
||||
var entries: seq[cstring] = @[]
|
||||
for i in 1..500:
|
||||
entries.add(cstring("Entry ") & &i)
|
||||
|
||||
proc scrollEvent(ev: Event; n: VNode) =
|
||||
let d = n.dom
|
||||
if d != nil and inViewport(d.lastChild):
|
||||
# "load" more data:
|
||||
for i in 1..50:
|
||||
entries.add(cstring("Loaded Entry ") & &i)
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml():
|
||||
tdiv(onscroll=scrollEvent, style=style(
|
||||
(StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
|
||||
for x in entries:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
1
examples/scrollapp/scrollapp.nims
Normal file
1
examples/scrollapp/scrollapp.nims
Normal file
|
|
@ -0,0 +1 @@
|
|||
--path: "../.."
|
||||
|
|
@ -1,19 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Todo app</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<head>
|
||||
<title>Todo app</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="nimcache/todoapp.js"></script>
|
||||
<script type="text/javascript">
|
||||
onload()
|
||||
</script>
|
||||
<script type="text/javascript" src="todoapp.js"></script>
|
||||
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,116 +1,152 @@
|
|||
|
||||
import vdom, karax, karaxdsl, jstrutils
|
||||
import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
|
||||
|
||||
type
|
||||
Filter = enum
|
||||
all, active, completed
|
||||
|
||||
var
|
||||
entries: seq[(cstring, bool)]
|
||||
selectedEntry = -1
|
||||
filter: Filter
|
||||
entriesLen: int
|
||||
doneswitch = true
|
||||
const
|
||||
contentSuffix = cstring"content"
|
||||
completedSuffix = cstring"completed"
|
||||
lenSuffix = cstring"entriesLen"
|
||||
|
||||
proc getEntryContent(pos: int): cstring =
|
||||
result = getItem(&pos & contentSuffix)
|
||||
if result == cstring"null":
|
||||
result = nil
|
||||
|
||||
proc isCompleted(pos: int): bool =
|
||||
var value = getItem(&pos & completedSuffix)
|
||||
result = value == cstring"true"
|
||||
|
||||
proc setEntryContent(pos: int, content: cstring) =
|
||||
setItem(&pos & contentSuffix, content)
|
||||
|
||||
proc markAsCompleted(pos: int, completed: bool) =
|
||||
setItem(&pos & completedSuffix, &completed)
|
||||
|
||||
proc addEntry(content: cstring, completed: bool) =
|
||||
setEntryContent(entriesLen, content)
|
||||
markAsCompleted(entriesLen, completed)
|
||||
inc entriesLen
|
||||
setItem(lenSuffix, &entriesLen)
|
||||
|
||||
proc updateEntry(pos: int, content: cstring, completed: bool) =
|
||||
setEntryContent(pos, content)
|
||||
markAsCompleted(pos, completed)
|
||||
|
||||
proc onTodoEnter(ev: Event; n: VNode) =
|
||||
entries.add((n.value, false))
|
||||
n.value = ""
|
||||
if n.value.strip() != "":
|
||||
addEntry(n.value, false)
|
||||
n.value = ""
|
||||
|
||||
proc removeHandler(ev: Event; n: VNode) =
|
||||
entries[n.key] = (cstring(nil), false)
|
||||
updateEntry(n.index, cstring(nil), false)
|
||||
|
||||
proc editHandler(ev: Event; n: VNode) =
|
||||
selectedEntry = n.key
|
||||
selectedEntry = n.index
|
||||
|
||||
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
|
||||
|
||||
proc editEntry(ev: Event; n: VNode) =
|
||||
entries[n.key][0] = n.value
|
||||
setEntryContent(n.index, n.value)
|
||||
selectedEntry = -1
|
||||
|
||||
proc toggleEntry(ev: Event; n: VNode) =
|
||||
let id = n.key
|
||||
entries[id][1] = not entries[id][1]
|
||||
let id = n.index
|
||||
markAsCompleted(id, not isCompleted(id))
|
||||
|
||||
proc onAllDone(ev: Event; n: VNode) =
|
||||
entries = @[]
|
||||
selectedEntry = -1
|
||||
|
||||
for i in 0..<entriesLen:
|
||||
markAsCompleted(i, doneswitch)
|
||||
doneswitch = not doneswitch
|
||||
proc clearCompleted(ev: Event, n: VNode) =
|
||||
for i in 0..<entries.len:
|
||||
if entries[i][1]: entries[i][0] = nil
|
||||
for i in 0..<entriesLen:
|
||||
if isCompleted(i): setEntryContent(i, nil)
|
||||
|
||||
proc toClass(completed: bool): cstring =
|
||||
(if completed: cstring"completed" else: cstring(nil))
|
||||
|
||||
proc toChecked(checked: bool): cstring =
|
||||
(if checked: cstring"checked" else: cstring(nil))
|
||||
|
||||
proc selected(v: Filter): cstring =
|
||||
(if filter == v: cstring"selected" else: cstring(nil))
|
||||
|
||||
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
|
||||
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compact.} =
|
||||
result = buildHtml(tr):
|
||||
li(class=toClass(completed)):
|
||||
if not selected:
|
||||
tdiv(class = "view"):
|
||||
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
|
||||
onclick=toggleEntry, key=id)
|
||||
label(onDblClick=editHandler, key=id):
|
||||
onclick=toggleEntry, index=id)
|
||||
label(onDblClick=editHandler, index=id):
|
||||
text d
|
||||
button(class = "destroy", key=id, onclick=removeHandler)
|
||||
button(class = "destroy", index=id, onclick=removeHandler)
|
||||
else:
|
||||
input(class = "edit", name = "title", key=id,
|
||||
onfocusLost = focusLost,
|
||||
onenter = editEntry, value = d, setFocus)
|
||||
input(class = "edit", name = "title", index=id,
|
||||
onblur = focusLost,
|
||||
onkeyupenter = editEntry, value = d, setFocus=true)
|
||||
|
||||
proc createDom(): VNode =
|
||||
proc makeFooter(entriesCount, completedCount: int): VNode =
|
||||
result = buildHtml(footer(class = "footer")):
|
||||
span(class = "todo-count"):
|
||||
strong:
|
||||
text(&entriesCount)
|
||||
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
|
||||
ul(class = "filters"):
|
||||
li:
|
||||
a(class = selected(all), href = "#/"):
|
||||
text "All"
|
||||
li:
|
||||
a(class = selected(active), href = "#/active"):
|
||||
text "Active"
|
||||
li:
|
||||
a(class = selected(completed), href = "#/completed"):
|
||||
text "Completed"
|
||||
button(class = "clear-completed", onclick = clearCompleted):
|
||||
text "Clear completed (" & &completedCount & ")"
|
||||
|
||||
proc makeHeader(): VNode {.compact.} =
|
||||
result = buildHtml(header(class = "header")):
|
||||
h1:
|
||||
text "todos"
|
||||
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
|
||||
onkeyupenter = onTodoEnter, setFocus)
|
||||
|
||||
proc createDom(data: RouterData): VNode =
|
||||
if data.hashPart == "#/": filter = all
|
||||
elif data.hashPart == "#/completed": filter = completed
|
||||
elif data.hashPart == "#/active": filter = active
|
||||
result = buildHtml(tdiv(class="todomvc-wrapper")):
|
||||
section(class = "todoapp"):
|
||||
header(class = "header"):
|
||||
h1:
|
||||
text "todos"
|
||||
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
|
||||
onenter = onTodoEnter, setFocus)
|
||||
makeHeader()
|
||||
section(class = "main"):
|
||||
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
|
||||
label(`for` = "toggle-all", onclick = onAllDone):
|
||||
input(class = "toggle-all", `type` = "checkbox", id = "toggle", onclick = onAllDone)
|
||||
label(`for` = "toggle"):
|
||||
text "Mark all as complete"
|
||||
var entriesCount = 0
|
||||
var completedCount = 0
|
||||
ul(class = "todo-list"):
|
||||
for i, d in pairs(entries):
|
||||
if d[0] != nil:
|
||||
#for i, d in pairs(entries):
|
||||
for i in 0..entriesLen-1:
|
||||
var d0 = getEntryContent(i)
|
||||
var d1 = isCompleted(i)
|
||||
if d0 != nil:
|
||||
let b = case filter
|
||||
of all: true
|
||||
of active: not d[1]
|
||||
of completed: d[1]
|
||||
of active: not d1
|
||||
of completed: d1
|
||||
if b:
|
||||
createEntry(i, d[0], d[1], i == selectedEntry)
|
||||
inc completedCount, ord(d[1])
|
||||
createEntry(i, d0, d1, i == selectedEntry)
|
||||
inc completedCount, ord(d1)
|
||||
inc entriesCount
|
||||
footer(class = "footer"):
|
||||
span(class = "todo-count"):
|
||||
strong:
|
||||
text(&entriesCount)
|
||||
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
|
||||
ul(class = "filters"):
|
||||
li:
|
||||
anchor(class = selected(all), href = "#/"):
|
||||
text "All"
|
||||
li:
|
||||
anchor(class = selected(active), href = "#/active"):
|
||||
text "Active"
|
||||
li:
|
||||
anchor(class = selected(completed), href = "#/completed"):
|
||||
text "Completed"
|
||||
button(class = "clear-completed", onclick = clearCompleted):
|
||||
text "Clear completed (" & &completedCount & ")"
|
||||
makeFooter(entriesCount, completedCount)
|
||||
|
||||
setOnHashChange(proc(hash: cstring) =
|
||||
if hash == cstring"#/": filter = all
|
||||
elif hash == cstring"#/completed": filter = completed
|
||||
elif hash == cstring"#/active": filter = active
|
||||
)
|
||||
if hasItem(lenSuffix):
|
||||
entriesLen = parseInt getItem(lenSuffix)
|
||||
else:
|
||||
entriesLen = 0
|
||||
setRenderer createDom
|
||||
|
||||
proc onload() {.exportc.} =
|
||||
init()
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
--path: "../../src"
|
||||
--path: "../.."
|
||||
|
|
|
|||
35
examples/toychat.nim
Normal file
35
examples/toychat.nim
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
include login
|
||||
|
||||
const
|
||||
message = "message"
|
||||
|
||||
type
|
||||
TextMessage = ref object
|
||||
name, content: kstring
|
||||
|
||||
var allMessages: seq[TextMessage] = @[]
|
||||
|
||||
proc doSendMessage() =
|
||||
let inputField = getVNodeById(message)
|
||||
allMessages.add(TextMessage(name: "you", content: inputField.getInputText))
|
||||
inputField.setInputText ""
|
||||
|
||||
proc main(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
loginDialog()
|
||||
tdiv:
|
||||
table:
|
||||
for m in allMessages:
|
||||
tr:
|
||||
td:
|
||||
bold:
|
||||
text m.name
|
||||
td:
|
||||
text m.content
|
||||
tdiv:
|
||||
if loggedIn:
|
||||
label(`for` = message):
|
||||
text "Message: "
|
||||
input(class = "input", id = message, onkeyupenter = doSendMessage)
|
||||
|
||||
setRenderer main
|
||||
13
experiments/ajaxtest.html
Normal file
13
experiments/ajaxtest.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Karax AJAX Test</title>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="ajaxtest.js"></script>
|
||||
</html>
|
||||
6
experiments/ajaxtest.nim
Normal file
6
experiments/ajaxtest.nim
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import kdom, kajax
|
||||
|
||||
proc cb(httpStatus: int, response: cstring) =
|
||||
echo "Worked!"
|
||||
|
||||
ajaxGet("https://httpbin.org/get", @[], cb)
|
||||
2
experiments/config.nims
Normal file
2
experiments/config.nims
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
--path: "../karax"
|
||||
--path: ".."
|
||||
14
experiments/echartstest.html
Normal file
14
experiments/echartstest.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Karax ECharts Test</title>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.0.2/echarts-en.js"></script>
|
||||
<script type="text/javascript" src="echartstest.js"></script>
|
||||
</html>
|
||||
58
experiments/echartstest.nim
Normal file
58
experiments/echartstest.nim
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
|
||||
|
||||
type
|
||||
Views = enum
|
||||
Customers, Products
|
||||
|
||||
var
|
||||
currentView = Customers
|
||||
|
||||
type
|
||||
EChart* = ref object
|
||||
|
||||
proc echartsInit(n: Element): EChart {.importc: "echarts.init".}
|
||||
proc setOption(x: EChart; option: JsonNode) {.importcpp.}
|
||||
|
||||
proc postRender(data: RouterData) =
|
||||
if currentView == Products:
|
||||
let myChart = echartsInit(kdom.getElementById("echartSection"))
|
||||
# specify chart configuration item and data
|
||||
let option = %*{
|
||||
"title": {
|
||||
"text": "ECharts entry example"
|
||||
},
|
||||
"tooltip": {},
|
||||
"legend": {
|
||||
"data": ["Sales"]
|
||||
},
|
||||
"xAxis": {
|
||||
"data": ["shirt","cardign","chiffon shirt","pants","heels","socks"]
|
||||
},
|
||||
"yAxis": {},
|
||||
"series": [{
|
||||
"name": "Sales",
|
||||
"type": "bar",
|
||||
"data": [5, 20, 36, 10, 10, 20]
|
||||
}]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
|
||||
proc createDom(data: RouterData): VNode =
|
||||
let hash = data.hashPart
|
||||
if hash == cstring"#/Products": currentView = Products
|
||||
else: currentView = Customers
|
||||
|
||||
result = buildHtml(tdiv):
|
||||
ul(class = "tabs"):
|
||||
for v in low(Views)..high(Views):
|
||||
li:
|
||||
a(href = "#/" & $v):
|
||||
text cstring($v)
|
||||
|
||||
if currentView == Products:
|
||||
tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))
|
||||
tdiv:
|
||||
text "other section"
|
||||
|
||||
setRenderer createDom, "ROOT", postRender
|
||||
setForeignNodeId "echartSection"
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Todo app</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="nimcache/example.js"></script>
|
||||
<script type="text/javascript">
|
||||
onload(window.location.pathname.split('/')[1])
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
|
||||
import vdom, karax, karaxdsl, jdict, jstrutils
|
||||
|
||||
type
|
||||
Filter = enum
|
||||
all, active, completed
|
||||
|
||||
var
|
||||
entries: seq[(cstring, bool)]
|
||||
selectedEntry = -1
|
||||
filter: Filter
|
||||
|
||||
proc onTodoEnter(ev: Event; n: VNode) =
|
||||
entries.add((n.value, false))
|
||||
n.value = ""
|
||||
|
||||
proc removeHandler(ev: Event; n: VNode) =
|
||||
let id = suffixAsInt(n.id, "remove:")
|
||||
entries[id] = (cstring(nil), false)
|
||||
|
||||
proc editHandler(ev: Event; n: VNode) =
|
||||
let id = suffixAsInt(n.id, "edit:")
|
||||
selectedEntry = id
|
||||
|
||||
when defined(usecache):
|
||||
var entryCache = newJDict[int, VNode]()
|
||||
|
||||
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
|
||||
|
||||
proc editEntry(ev: Event; n: VNode) =
|
||||
let id = suffixAsInt(n.id, "todo-edit:")
|
||||
entries[id][0] = n.value
|
||||
selectedEntry = -1
|
||||
|
||||
proc toggleEntry(ev: Event; n: VNode) =
|
||||
let id = suffixAsInt(n.id, "toggle:")
|
||||
entries[id][1] = not entries[id][1]
|
||||
|
||||
proc onAllDone(ev: Event; n: VNode) =
|
||||
entries = @[]
|
||||
selectedEntry = -1
|
||||
|
||||
proc clearCompleted(ev: Event, n: VNode) =
|
||||
for i in 0..<entries.len:
|
||||
if entries[i][1]: entries[i][0] = nil
|
||||
|
||||
proc toClass(completed: bool): cstring =
|
||||
(if completed: cstring"completed" else: cstring"")
|
||||
|
||||
proc toVis(visible: bool): cstring =
|
||||
(if visible: cstring"visible" else: "hidden")
|
||||
|
||||
proc toChecked(checked: bool): cstring =
|
||||
(if checked: cstring"checked" else: cstring(nil))
|
||||
|
||||
proc selected(v: Filter): cstring =
|
||||
(if filter == v: cstring"selected" else: cstring(nil))
|
||||
|
||||
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
|
||||
# implement caching:
|
||||
when defined(usecache):
|
||||
if entryCache.contains(i):
|
||||
let old = entryCache[i]
|
||||
return old
|
||||
|
||||
result = buildHtml(tr):
|
||||
li(class=toClass(completed)):
|
||||
if not selected:
|
||||
tdiv(class = "view"):
|
||||
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
|
||||
onclick=toggleEntry, id="toggle:" & &id)
|
||||
label(onDblClick=editHandler, id="edit:" & &id):
|
||||
text d
|
||||
button(class = "destroy", id="remove:" & $id, onclick=removeHandler)
|
||||
else:
|
||||
input(class = "edit", name = "title", id = "todo-edit:" & &id,
|
||||
onfocusLost = focusLost,
|
||||
onenter = editEntry, value = d)
|
||||
when defined(usecache):
|
||||
entryCache[i] = result
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv(class="todomvc-wrapper")):
|
||||
section(class = "todoapp"):
|
||||
header(class = "header"):
|
||||
h1:
|
||||
text "todos"
|
||||
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
|
||||
onenter = onTodoEnter)
|
||||
section(class = "main"):
|
||||
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
|
||||
label(`for` = "toggle-all", onclick = onAllDone):
|
||||
text "Mark all as complete"
|
||||
var entriesCount = 0
|
||||
var completedCount = 0
|
||||
ul(class = "todo-list"):
|
||||
for i, d in pairs(entries):
|
||||
if d[0] != nil:
|
||||
let b = case filter
|
||||
of all: true
|
||||
of active: not d[1]
|
||||
of completed: d[1]
|
||||
if b:
|
||||
createEntry(i, d[0], d[1], i == selectedEntry)
|
||||
inc completedCount, ord(d[1])
|
||||
inc entriesCount
|
||||
footer(class = "footer"):
|
||||
span(class = "todo-count"):
|
||||
strong:
|
||||
text(&entriesCount)
|
||||
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
|
||||
ul(class = "filters"):
|
||||
li:
|
||||
anchor(class = selected(all), href = "#/"):
|
||||
text "All"
|
||||
li:
|
||||
anchor(class = selected(active), href = "#/active"):
|
||||
text "Active"
|
||||
li:
|
||||
anchor(class = selected(completed), href = "#/completed"):
|
||||
text "Completed"
|
||||
button(class = "clear-completed", onclick = clearCompleted):
|
||||
text "Clear completed (" & &completedCount & ")"
|
||||
|
||||
setOnHashChange(proc(hash: cstring) =
|
||||
if hash == cstring"#/": filter = all
|
||||
elif hash == cstring"#/completed": filter = completed
|
||||
elif hash == cstring"#/active": filter = active
|
||||
)
|
||||
setRenderer createDom
|
||||
|
||||
proc onload(session: cstring) {.exportc.} =
|
||||
for i in 0..1: # 0_000:
|
||||
entries.add((cstring"Entry " & &i, false))
|
||||
init()
|
||||
65
experiments/menus.nim
Normal file
65
experiments/menus.nim
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
## Example program that shows how to create menus with Karax.
|
||||
|
||||
include prelude
|
||||
import jstrutils, kdom
|
||||
|
||||
proc contentA(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
text "content A"
|
||||
|
||||
proc contentB(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
text "content B"
|
||||
|
||||
proc contentC(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
text "content C"
|
||||
|
||||
|
||||
|
||||
type
|
||||
MenuItemHandler = proc(): VNode
|
||||
|
||||
var content: MenuItemHandler = contentA
|
||||
|
||||
proc menuAction(x: MenuItemHandler): proc() =
|
||||
result = proc() = content = x
|
||||
|
||||
proc buildMenu(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
nav(class="navbar is-primary"):
|
||||
tdiv(class="navbar-brand"):
|
||||
a(class="navbar-item", onclick = menuAction(contentA)):
|
||||
strong:
|
||||
text "My awesome menu"
|
||||
|
||||
tdiv(id="navMenuTransparentExample", class="navbar-menu"):
|
||||
tdiv(class = "navbar-start"):
|
||||
tdiv(class="navbar-item has-dropdown is-hoverable"):
|
||||
a(class="navbar-link", onclick = menuAction(contentB)):
|
||||
text "Masters"
|
||||
tdiv(class="navbar-dropdown is-boxed"):
|
||||
a(class="navbar-item", onclick = menuAction(contentC)):
|
||||
text "Inventory"
|
||||
a(class="navbar-item", onclick = menuAction(contentC)):
|
||||
text "Product"
|
||||
hr(class="navbar-divider"):
|
||||
a(class="navbar-item", onclick = menuAction(contentC)):
|
||||
text "Product"
|
||||
|
||||
tdiv(class="navbar-item has-dropdown is-hoverable"):
|
||||
a(class="navbar-link", onclick = menuAction(contentC)):
|
||||
text "Transactions"
|
||||
tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"):
|
||||
a(class="navbar-item", onclick = menuAction(contentC)):
|
||||
text "Purchase Bill"
|
||||
a(class="navbar-item", onclick = menuAction(contentC)):
|
||||
text "Purchase Return"
|
||||
a(class="navbar-item", onclick = menuAction(contentC)):
|
||||
text "Sale Bill"
|
||||
hr(class="navbar-divider"):
|
||||
a(class="navbar-item", onclick = menuAction(contentC)):
|
||||
text "Stock Adjustment"
|
||||
content()
|
||||
|
||||
setRenderer buildMenu
|
||||
18
experiments/nativenodes.nim
Normal file
18
experiments/nativenodes.nim
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
|
||||
include karax / prelude
|
||||
import karax / kdom
|
||||
|
||||
proc myInput: VNode =
|
||||
result = buildHtml:
|
||||
input()
|
||||
|
||||
var inp = vnodeToDom(myInput())
|
||||
|
||||
proc myAwesomeComponent(x: Node): VNode =
|
||||
result = dthunk(x)
|
||||
|
||||
proc main: VNode =
|
||||
result = myAwesomeComponent(inp)
|
||||
|
||||
setRenderer main
|
||||
120
experiments/nextgen.nim
Normal file
120
experiments/nextgen.nim
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
|
||||
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive
|
||||
|
||||
proc textInput*(text: RString; focus: RBool): VNode {.track.} =
|
||||
proc onFlip(ev: Event; target: VNode) =
|
||||
focus <- not focus.value
|
||||
|
||||
proc onKeyupEnter(ev: Event; target: VNode) =
|
||||
text <- target.value
|
||||
|
||||
proc onkeyup(ev: Event; n: VNode) =
|
||||
# keep displayValue up to date, but do not tell the client yet!
|
||||
text.value = n.value
|
||||
|
||||
result = buildHtml(input(`type`="text",
|
||||
value=text.value, onblur=onFlip, onfocus=onFlip,
|
||||
onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value))
|
||||
|
||||
var
|
||||
errmsg = rstr("")
|
||||
|
||||
makeReactive:
|
||||
type
|
||||
User = ref object
|
||||
firstname, lastname: cstring
|
||||
selected: bool
|
||||
|
||||
var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")),
|
||||
(User(rawFirstname: "Some", rawLastname: "One")),
|
||||
(User(rawFirstname: "Some", rawLastname: "Two"))])
|
||||
var prevSelected: User = nil #newReactive[User](nil)
|
||||
|
||||
proc unselect() =
|
||||
if prevSelected != nil:
|
||||
prevSelected.selected = false
|
||||
prevSelected = nil
|
||||
|
||||
proc select(u: User) =
|
||||
unselect()
|
||||
u.selected = true
|
||||
prevSelected = u
|
||||
|
||||
proc toUI*(isFirstname: bool): RString =
|
||||
result = RString()
|
||||
result.subscribe proc (v: cstring) =
|
||||
if v.len > 0:
|
||||
let p = prevSelected #selected.value
|
||||
if p != nil:
|
||||
if isFirstName:
|
||||
p.firstname = v
|
||||
else:
|
||||
p.lastname = v
|
||||
unselect()
|
||||
errmsg <- ""
|
||||
else:
|
||||
errmsg <- "name must not be empty"
|
||||
|
||||
var inpFirstname = toUI(true)
|
||||
var inpLastname = toUI(false)
|
||||
|
||||
proc adaptFocus(def = false): RBool =
|
||||
result = RBool()
|
||||
result.value = def
|
||||
when false:
|
||||
result.subscribe proc (hasFocus: bool) =
|
||||
if not hasFocus:
|
||||
unselect()
|
||||
|
||||
var focusA = adaptFocus()
|
||||
var focusB = adaptFocus()
|
||||
|
||||
proc styler(): VStyle =
|
||||
result = style(
|
||||
(StyleAttr.position, cstring"relative"),
|
||||
(StyleAttr.paddingLeft, cstring"10px"),
|
||||
(StyleAttr.paddingRight, cstring"5px"),
|
||||
(StyleAttr.height, cstring"30px"),
|
||||
(StyleAttr.lineHeight, cstring"30px"),
|
||||
(StyleAttr.border, cstring"solid 8px " & (if focusA.value: cstring"red" else: cstring"black")),
|
||||
(StyleAttr.fontSize, cstring"12px"),
|
||||
(StyleAttr.fontWeight, cstring"600")
|
||||
)
|
||||
|
||||
var clicks = 0
|
||||
|
||||
proc renderUser(u: User): VNode {.track.} =
|
||||
result = buildHtml(tdiv):
|
||||
if u.selected:
|
||||
inpFirstname <- u.firstname
|
||||
tdiv:
|
||||
textInput inpFirstname, focusA
|
||||
inpLastname <- u.lastname
|
||||
tdiv:
|
||||
textInput inpLastname, focusB
|
||||
else:
|
||||
button:
|
||||
text "..."
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
select(u)
|
||||
text u.firstname & " " & u.lastname
|
||||
button:
|
||||
text "(x)"
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
gu.deleteElem(u)
|
||||
|
||||
proc main(gu: RSeq[User]): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
tdiv:
|
||||
button:
|
||||
text "Add User"
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
inc clicks
|
||||
gu.add User(rawFirstname: "Added", rawLastname: &clicks)
|
||||
tdiv:
|
||||
text errmsg
|
||||
vmapIt(gu, tdiv, renderUser(it))
|
||||
|
||||
proc init(): VNode = main(gu)
|
||||
|
||||
setInitializer(init)
|
||||
251
experiments/oldwidgets.nim
Normal file
251
experiments/oldwidgets.nim
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
var
|
||||
linkCounter: int
|
||||
|
||||
proc link*(id: int): VNode =
|
||||
result = newVNode(VNodeKind.anchor)
|
||||
result.setAttr("href", "#")
|
||||
inc linkCounter
|
||||
result.setAttr("id", $linkCounter & ":" & $id)
|
||||
|
||||
proc link*(action: EventHandler): VNode =
|
||||
result = newVNode(VNodeKind.anchor)
|
||||
result.setAttr("href", "#")
|
||||
result.setOnclick action
|
||||
|
||||
when false:
|
||||
proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
|
||||
result = newVNode(VNodeKind.button)
|
||||
result.add text(caption)
|
||||
if action != nil:
|
||||
result.setOnClick action
|
||||
if disabled:
|
||||
result.setAttr("disabled", "true")
|
||||
|
||||
proc select*(choices: openarray[cstring]): VNode =
|
||||
result = newVNode(VNodeKind.select)
|
||||
var i = 0
|
||||
for c in choices:
|
||||
result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c))
|
||||
inc i
|
||||
|
||||
proc select*(choices: openarray[(int, cstring)]): VNode =
|
||||
result = newVNode(VNodeKind.select)
|
||||
for c in choices:
|
||||
result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1]))
|
||||
|
||||
var radioCounter: int
|
||||
|
||||
proc radio*(choices: openarray[(int, cstring)]): VNode =
|
||||
result = newVNode(VNodeKind.fieldset)
|
||||
var i = 0
|
||||
inc radioCounter
|
||||
for c in choices:
|
||||
let id = cstring"radio_" & c[1] & toCstr(i)
|
||||
var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"),
|
||||
(cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)),
|
||||
(cstring"value", toCStr(c[0]))])
|
||||
if i == 0:
|
||||
kid.setAttr(cstring"checked", cstring"checked")
|
||||
var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1]))
|
||||
kid.add lab
|
||||
result.add kid
|
||||
inc i
|
||||
|
||||
proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode =
|
||||
result = newVNode(kind)
|
||||
result.id = id
|
||||
result.class = class
|
||||
|
||||
proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class)
|
||||
proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class)
|
||||
|
||||
proc valueAsInt*(e: Node): int = parseInt(e.value)
|
||||
|
||||
proc th*(s: cstring): VNode =
|
||||
result = newVNode(VNodeKind.th)
|
||||
result.add text(s)
|
||||
|
||||
proc td*(s: string): VNode =
|
||||
result = newVNode(VNodeKind.td)
|
||||
result.add text(s)
|
||||
|
||||
proc td*(s: VNode): VNode =
|
||||
result = newVNode(VNodeKind.td)
|
||||
result.add s
|
||||
|
||||
proc td*(class: cstring; s: VNode): VNode =
|
||||
result = newVNode(VNodeKind.td)
|
||||
result.add s
|
||||
result.class = class
|
||||
|
||||
proc table*(class=cstring(nil), kids: varargs[VNode]): VNode =
|
||||
result = tag(VNodeKind.table, nil, class)
|
||||
for k in kids: result.add k
|
||||
|
||||
proc tr*(kids: varargs[VNode]): VNode =
|
||||
result = newVNode(VNodeKind.tr)
|
||||
for k in kids:
|
||||
if k.kind in {VNodeKind.td, VNodeKind.th}:
|
||||
result.add k
|
||||
else:
|
||||
result.add td(k)
|
||||
|
||||
proc suffix*(s, prefix: cstring): cstring =
|
||||
if s.startsWith(prefix):
|
||||
result = s.substr(prefix.len)
|
||||
else:
|
||||
kout(cstring"bug! " & s & cstring" does not start with " & prefix)
|
||||
|
||||
proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
|
||||
|
||||
#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.}
|
||||
|
||||
proc realtimeInput*(val: cstring; action: EventHandler): VNode =
|
||||
var timer: Timeout
|
||||
proc onkeyup(ev: Event; n: VNode) =
|
||||
proc wrapper() = keyeventBody()
|
||||
|
||||
if timer != nil: clearTimeout(timer)
|
||||
timer = setTimeout(wrapper, 400)
|
||||
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
|
||||
result.value = val
|
||||
result.addEventListener(EventKind.onkeyup, onkeyup)
|
||||
|
||||
proc enterInput*(id, val: cstring; action: EventHandler): VNode =
|
||||
proc onkeyup(ev: Event; n: VNode) =
|
||||
if ev.keyCode == 13: keyeventBody()
|
||||
|
||||
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
|
||||
result.id = id
|
||||
result.value = val
|
||||
result.addEventListener(EventKind.onkeyup, onkeyup)
|
||||
|
||||
proc setOnEnter*(n: VNode; action: EventHandler) =
|
||||
proc onkeyup(ev: Event; n: VNode) =
|
||||
if ev.keyCode == 13: keyeventBody()
|
||||
n.addEventListener(EventKind.onkeyup, onkeyup)
|
||||
|
||||
proc setOnscroll*(action: proc(min, max: VKey; diff: int)) =
|
||||
var oldY = window.pageYOffset
|
||||
|
||||
proc wrapper(ev: Event) =
|
||||
let dir = window.pageYOffset - oldY
|
||||
if dir == 0: return
|
||||
|
||||
var a = VKey high(int)
|
||||
var b = VKey 0
|
||||
var h, count: int
|
||||
document.visibleKeys(a, b, h, count)
|
||||
let avgh = h / count
|
||||
let diff = toInt(dir.float / avgh)
|
||||
if diff != 0:
|
||||
oldY = window.pageYOffset
|
||||
action(a, b, diff)
|
||||
redraw()
|
||||
|
||||
document.addEventListener("scroll", wrapper)
|
||||
|
||||
when false:
|
||||
var plugins {.exportc.}: seq[(string, proc())] = @[]
|
||||
|
||||
proc onInput(val: cstring) =
|
||||
kout val
|
||||
if val == "dyn":
|
||||
kout(plugins.len)
|
||||
if plugins.len > 0:
|
||||
plugins[0][1]()
|
||||
|
||||
var
|
||||
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
|
||||
|
||||
proc carousel*(): VNode =
|
||||
var currentIndex = 0
|
||||
|
||||
proc next(ev: Event; n: VNode) =
|
||||
currentIndex = (currentIndex + 1) mod images.len
|
||||
|
||||
proc prev(ev: Event; n: VNode) =
|
||||
currentIndex = (currentIndex - 1) mod images.len
|
||||
|
||||
result = buildHtml(tdiv):
|
||||
text images[currentIndex]
|
||||
button(onclick = next):
|
||||
text "Next"
|
||||
button(onclick = prev):
|
||||
text "Previous"
|
||||
|
||||
#proc targetElem*(e: Event): Element = cast[Element](e.target)
|
||||
|
||||
#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
|
||||
# "document.getElementsByClassName", nodecl.}
|
||||
#proc textContent(e: Node): cstring {.
|
||||
# importcpp: "#.textContent", nodecl.}
|
||||
|
||||
proc isElementInViewport(el: Node; h: var int): bool =
|
||||
let rect = el.getBoundingClientRect()
|
||||
h = rect.bottom - rect.top
|
||||
result = rect.top >= 0 and rect.left >= 0 and
|
||||
rect.bottom <= clientHeight() and
|
||||
rect.right <= clientWidth()
|
||||
|
||||
proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
|
||||
# we only care about nodes that have a key:
|
||||
var hh = 0
|
||||
# do not recurse if there is a 'key' field already:
|
||||
if e.key >= 0:
|
||||
if isElementInViewport(e, hh):
|
||||
inc count
|
||||
inc h, hh
|
||||
a = min(a, e.key)
|
||||
b = max(b, e.key)
|
||||
else:
|
||||
for i in 0..<e.len:
|
||||
visibleKeys(e[i], a, b, h, count)
|
||||
|
||||
template toState(x): untyped = "state" & x
|
||||
proc accessState(sv: string): NimNode {.compileTime.} =
|
||||
newTree(nnkBracketExpr, newIdentNode(sv), newIdentNode("key"))
|
||||
|
||||
proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl, init: NimNode) =
|
||||
case n.kind
|
||||
of nnkVarSection, nnkLetSection:
|
||||
for c in n:
|
||||
expectKind c, nnkIdentDefs
|
||||
let typ = c[^2]
|
||||
let val = c[^1]
|
||||
let usedType = if typ.kind != nnkEmpty: typ else: val
|
||||
if usedType.kind == nnkEmpty:
|
||||
error("cannot determine the variable's type", c)
|
||||
for i in 0 .. c.len-3:
|
||||
let v = $c[i]
|
||||
let sv = toState v
|
||||
decl.add quote do:
|
||||
var `sv` = newJDict[VKey, `usedType`]()
|
||||
if val.kind != nnkEmpty:
|
||||
init.add newTree(nnkAsgn, accessState(sv), val)
|
||||
names[v] = true
|
||||
of nnkStmtList, nnkStmtListExpr:
|
||||
for x in n: stateDecl(x, names, decl, init)
|
||||
of nnkDo:
|
||||
stateDecl(n.body, names, decl, init)
|
||||
of nnkCommentStmt: discard
|
||||
else:
|
||||
error("invalid 'state' declaration", n)
|
||||
|
||||
proc doState(n: NimNode; names: TableRef[string, bool];
|
||||
decl, init: NimNode): NimNode =
|
||||
result = n
|
||||
case n.kind
|
||||
of nnkCallKinds:
|
||||
# handle 'state' declaration and remove it from the AST:
|
||||
if n.len == 2 and repr(n[0]) == "state":
|
||||
stateDecl(n[1], names, decl, init)
|
||||
result = newTree(nnkEmpty)
|
||||
of nnkSym, nnkIdent:
|
||||
let v = $n
|
||||
if v in names:
|
||||
let sv = toState v
|
||||
result = accessState(sv)
|
||||
else:
|
||||
for i in 0..<n.len:
|
||||
result[i] = doState(n[i], names, decl, init)
|
||||
13
experiments/refs/demo.html
Normal file
13
experiments/refs/demo.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8"/>
|
||||
<title>Refs</title>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
<div id="ROOT"></div>
|
||||
<script type="text/javascript" src="demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
58
experiments/refs/demo.nim
Normal file
58
experiments/refs/demo.nim
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
include karaxprelude
|
||||
import future, sequtils
|
||||
|
||||
type
|
||||
CustomRef = ref object of VComponent
|
||||
|
||||
method onAttach(r: CustomRef) =
|
||||
kout(cstring"custom ref attached")
|
||||
|
||||
method onDetach(r: CustomRef) =
|
||||
kout(cstring"custom ref detached")
|
||||
|
||||
var
|
||||
modelData = @[5, 2, 4]
|
||||
|
||||
refA = VComponent()
|
||||
refB = VComponent()
|
||||
refC = CustomRef()
|
||||
refSeq = newSeq[VComponent]()
|
||||
|
||||
proc onClick(ev: Event, n: VNode) =
|
||||
modelData.add(0)
|
||||
|
||||
proc showRefs() =
|
||||
kout(refA)
|
||||
kout(refB)
|
||||
kout(refC)
|
||||
for i in 0 ..< refSeq.len:
|
||||
kout(refSeq[i].vnode)
|
||||
#kout(refSeq.map(nref => nref.vnode))
|
||||
|
||||
proc secureRefSlot(i: int): VComponent =
|
||||
while refSeq.len <= i:
|
||||
refSeq.add(VComponent())
|
||||
result = refSeq[i]
|
||||
|
||||
proc view(): VNode =
|
||||
result = buildHtml():
|
||||
tdiv:
|
||||
button(onclick=onClick):
|
||||
text "click me"
|
||||
# storing refs to single elements is straightforward now
|
||||
if modelData.len mod 2 == 0:
|
||||
tdiv(nref=refA):
|
||||
text "A"
|
||||
tdiv(nref=refB):
|
||||
text "B"
|
||||
tdiv(nref=refC):
|
||||
text "C"
|
||||
# It's a bit more tricky when containers are involved:
|
||||
for i, x in modelData.pairs:
|
||||
tdiv(nref=secureRefSlot(i)):
|
||||
text "List item: " & $x
|
||||
|
||||
proc renderer(): VNode =
|
||||
view()
|
||||
|
||||
setRenderer(renderer, "ROOT", showRefs)
|
||||
1
experiments/refs/demo.nims
Normal file
1
experiments/refs/demo.nims
Normal file
|
|
@ -0,0 +1 @@
|
|||
--path: "../../src"
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
|
||||
<script type="text/javascript" src="scrollapp.js"></script>
|
||||
<script type="text/javascript">
|
||||
onload(window.location.pathname.split('/')[1])
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
|
||||
import vdom, karax, karaxdsl, jdict, jstrutils
|
||||
import vdom, karax, karaxdsl, jdict, jstrutils, compact
|
||||
|
||||
type
|
||||
Filter = enum
|
||||
all, active, completed
|
||||
|
||||
const
|
||||
WindowSize = 50 # overestimating it does not harm
|
||||
|
||||
var
|
||||
entries: seq[(cstring, bool)]
|
||||
visibleA = 0
|
||||
visibleB = 10_000
|
||||
visibleB = 10
|
||||
selectedEntry = -1
|
||||
filter: Filter
|
||||
|
||||
|
|
@ -52,34 +49,15 @@ proc toChecked(checked: bool): cstring =
|
|||
proc selected(v: Filter): cstring =
|
||||
(if filter == v: cstring"selected" else: cstring(nil))
|
||||
|
||||
when false:
|
||||
proc entry(id: int, d: cstring; completed, selected: bool): proc (): VNode =
|
||||
var lastid: type(id)
|
||||
var lastd: type(d)
|
||||
var lastCompleted: type(completed)
|
||||
var lastSelected: type(selected)
|
||||
proc inner(): VNode =
|
||||
if lastid != id or lastd != d or lastCompleted != completed or lastSelected != selected:
|
||||
result = logic(id, d, completed, selected)
|
||||
lastid = id
|
||||
lastd = d
|
||||
lastCompleted = completed
|
||||
lastSelected = selected
|
||||
|
||||
result = inner
|
||||
|
||||
proc lazyEntry(args: seq[VNode]): VNode =
|
||||
let id = args[0].intValue
|
||||
let d = args[1].text
|
||||
let completed = args[2].intValue != 0
|
||||
let selected = args[3].intValue != 0
|
||||
proc createEntry(id: int; d: cstring;
|
||||
completed: bool, selected: bool): VNode {.component.} =
|
||||
result = buildHtml(tr):
|
||||
li(class=toClass(completed), key = id):
|
||||
if not selected:
|
||||
tdiv(class = "view"):
|
||||
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
|
||||
onclick=toggleEntry, key = id)
|
||||
label(onDblClick=editHandler, key = id):
|
||||
label(onclick=editHandler, key = id):
|
||||
text d
|
||||
button(class = "destroy", key = id, onclick = removeHandler)
|
||||
else:
|
||||
|
|
@ -87,9 +65,6 @@ proc lazyEntry(args: seq[VNode]): VNode =
|
|||
onfocusLost = focusLost,
|
||||
onenter = editEntry, value = d, setFocus)
|
||||
|
||||
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
|
||||
result = thunk(lazyEntry, id, d, completed, selected)
|
||||
|
||||
proc myscroll(ev: Event; n: VNode) =
|
||||
kout cstring"scrolling event here"
|
||||
|
||||
|
|
@ -105,10 +80,6 @@ proc createDom(): VNode =
|
|||
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
|
||||
label(`for` = "toggle-all", onclick = onAllDone):
|
||||
text "Mark all as complete"
|
||||
tdiv(onscroll = myscroll, style = "overflow: visible; width: 0px; display: block; max-height: 1em"):
|
||||
for i in 0..4:
|
||||
tdiv:
|
||||
text entries[i][0]
|
||||
|
||||
var entriesCount = 0
|
||||
var completedCount = 0
|
||||
|
|
|
|||
25
experiments/scrollapp_simple.nim
Normal file
25
experiments/scrollapp_simple.nim
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
## Example that shows how to accomplish an "infinitely scrolling" app.
|
||||
|
||||
include karax / prelude
|
||||
import karax / [kdom, vstyles]
|
||||
|
||||
var entries: seq[cstring] = @[]
|
||||
for i in 1..500:
|
||||
entries.add(cstring("Entry ") & &i)
|
||||
|
||||
proc scrollEvent(ev: Event; n: VNode) =
|
||||
let d = n.dom
|
||||
if d != nil and inViewport(d.lastChild):
|
||||
# "load" more data:
|
||||
for i in 1..50:
|
||||
entries.add(cstring("Loaded Entry ") & &i)
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml():
|
||||
tdiv(onscroll=scrollEvent, style=style(
|
||||
(StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
|
||||
for x in entries:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
378
experiments/style.css
Normal file
378
experiments/style.css
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todomvc-wrapper {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-font-smoothing: antialiased;
|
||||
font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #4d4d4d;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-font-smoothing: antialiased;
|
||||
font-smoothing: antialiased;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="checkbox"] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
font-size: 100px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: rgba(175, 47, 47, 0.15);
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
border: 0;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-font-smoothing: antialiased;
|
||||
font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
label[for='toggle-all'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
position: absolute;
|
||||
top: -55px;
|
||||
left: -12px;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
text-align: center;
|
||||
border: none; /* Mobile Safari */
|
||||
}
|
||||
|
||||
.toggle-all:before {
|
||||
content: '>';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: 506px;
|
||||
padding: 13px 17px 12px 17px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:after {
|
||||
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked:after {
|
||||
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
padding: 15px 60px 15px 15px;
|
||||
margin-left: 45px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: 'x';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a.selected,
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #bfbfbf;
|
||||
/* font-size: 10px; */
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
752
experiments/trello/knete.nim
Normal file
752
experiments/trello/knete.nim
Normal file
|
|
@ -0,0 +1,752 @@
|
|||
## Raw DOM manipulation. No DOM diff'ing, no cry.
|
||||
|
||||
import macros, tables
|
||||
from strutils import startsWith, toLowerAscii, toUpperAscii
|
||||
|
||||
when defined(js):
|
||||
type kstring* = cstring
|
||||
else:
|
||||
type kstring* = string
|
||||
|
||||
type
|
||||
Tag* {.pure.} = enum
|
||||
text = "#text",
|
||||
|
||||
html, head, title, base, link, meta, style,
|
||||
script, noscript,
|
||||
body, section, nav, article, aside,
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
header, footer, address, main
|
||||
|
||||
p, hr, pre, blockquote, ol, ul, li,
|
||||
dl, dt, dd,
|
||||
figure, figcaption,
|
||||
|
||||
tdiv = "div",
|
||||
|
||||
a, em, strong, small,
|
||||
strikethrough = "s", cite, quote,
|
||||
dfn, abbr, data, time, code, `var` = "var", samp,
|
||||
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
|
||||
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
|
||||
ins, del, img, iframe, embed, `object` = "object",
|
||||
param, video, audio, source, track, canvas, map,
|
||||
area, svg, math,
|
||||
|
||||
table, caption, colgroup, col, tbody, thead,
|
||||
tfoot, tr, td, th,
|
||||
|
||||
form, fieldset, legend, label, input, button,
|
||||
select, datalist, optgroup, option, textarea,
|
||||
keygen, output, progress, meter,
|
||||
details, summary, command, menu
|
||||
|
||||
type
|
||||
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
|
||||
onclick, ## An element is clicked.
|
||||
oncontextmenu, ## An element is right-clicked.
|
||||
ondblclick, ## An element is double clicked.
|
||||
onkeyup, ## A key was released.
|
||||
onkeydown, ## A key is pressed.
|
||||
onkeypressed, # A key was pressed.
|
||||
onfocus, ## An element got the focus.
|
||||
onblur, ## An element lost the focus.
|
||||
onchange, ## The selected value of an element was changed.
|
||||
onscroll, ## The user scrolled within an element.
|
||||
|
||||
onmousedown, ## A pointing device button (usually a mouse) is pressed
|
||||
## on an element.
|
||||
onmouseenter, ## A pointing device is moved onto the element that
|
||||
## has the listener attached.
|
||||
onmouseleave, ## A pointing device is moved off the element that
|
||||
## has the listener attached.
|
||||
onmousemove, ## A pointing device is moved over an element.
|
||||
onmouseout, ## A pointing device is moved off the element that
|
||||
## has the listener attached or off one of its children.
|
||||
onmouseover, ## A pointing device is moved onto the element that has
|
||||
## the listener attached or onto one of its children.
|
||||
onmouseup, ## A pointing device button is released over an element.
|
||||
|
||||
ondrag, ## An element or text selection is being dragged (every 350ms).
|
||||
ondragend, ## A drag operation is being ended (by releasing a mouse button
|
||||
## or hitting the escape key).
|
||||
ondragenter, ## A dragged element or text selection enters a valid drop target.
|
||||
ondragleave, ## A dragged element or text selection leaves a valid drop target.
|
||||
ondragover, ## An element or text selection is being dragged over a valid
|
||||
## drop target (every 350ms).
|
||||
ondragstart, ## The user starts dragging an element or text selection.
|
||||
ondrop, ## An element is dropped on a valid drop target.
|
||||
|
||||
onsubmit, ## A form is submitted
|
||||
oninput, ## An input value changes
|
||||
|
||||
onkeyupenter, ## vdom extension: an input field received the ENTER key press
|
||||
onkeyuplater ## vdom extension: a key was pressed and some time
|
||||
## passed (useful for on-the-fly text completions)
|
||||
|
||||
|
||||
macro buildLookupTables(): untyped =
|
||||
var a = newTree(nnkBracket)
|
||||
for i in low(Tag)..high(Tag):
|
||||
let x = $i
|
||||
let y = if x[0] == '#': x else: toUpperAscii(x)
|
||||
a.add(newCall("kstring", newLit(y)))
|
||||
var e = newTree(nnkBracket)
|
||||
for i in low(EventKind)..high(EventKind):
|
||||
e.add(newCall("kstring", newLit(substr($i, 2))))
|
||||
|
||||
template tmpl(a, e) {.dirty.} =
|
||||
const
|
||||
toTagName*: array[Tag, kstring] = a
|
||||
toEventName*: array[EventKind, kstring] = e
|
||||
|
||||
result = getAst tmpl(a, e)
|
||||
|
||||
buildLookupTables()
|
||||
|
||||
# ---------------- CSS ----------------------------------------------
|
||||
|
||||
type
|
||||
StyleAttr* {.pure.} = enum
|
||||
## The style attributes supported by the virtual DOM.
|
||||
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
|
||||
alignContent
|
||||
alignItems
|
||||
alignSelf
|
||||
animation
|
||||
animationDelay
|
||||
animationDirection
|
||||
animationDuration
|
||||
animationFillMode
|
||||
animationIterationCount
|
||||
animationName
|
||||
animationTimingFunction
|
||||
animationPlayState
|
||||
background
|
||||
backgroundAttachment
|
||||
backgroundColor
|
||||
backgroundImage
|
||||
backgroundPosition
|
||||
backgroundRepeat
|
||||
backgroundClip
|
||||
backgroundOrigin
|
||||
backgroundSize
|
||||
backfaceVisibility
|
||||
border
|
||||
borderBottom
|
||||
borderBottomColor
|
||||
borderBottomLeftRadius
|
||||
borderBottomRightRadius
|
||||
borderBottomStyle
|
||||
borderBottomWidth
|
||||
borderCollapse
|
||||
borderColor
|
||||
borderImage
|
||||
borderImageOutset
|
||||
borderImageRepeat
|
||||
borderImageSlice
|
||||
borderImageSource
|
||||
borderImageWidth
|
||||
borderLeft
|
||||
borderLeftColor
|
||||
borderLeftStyle
|
||||
borderLeftWidth
|
||||
borderRadius
|
||||
borderRight
|
||||
borderRightColor
|
||||
borderRightStyle
|
||||
borderRightWidth
|
||||
borderSpacing
|
||||
borderStyle
|
||||
borderTop
|
||||
borderTopColor
|
||||
borderTopLeftRadius
|
||||
borderTopRightRadius
|
||||
borderTopStyle
|
||||
borderTopWidth
|
||||
borderWidth
|
||||
bottom
|
||||
boxDecorationBreak
|
||||
boxShadow
|
||||
boxSizing
|
||||
captionSide
|
||||
clear
|
||||
clip
|
||||
color
|
||||
columnCount
|
||||
columnFill
|
||||
columnGap
|
||||
columnRule
|
||||
columnRuleColor
|
||||
columnRuleStyle
|
||||
columnRuleWidth
|
||||
columns
|
||||
columnSpan
|
||||
columnWidth
|
||||
content
|
||||
counterIncrement
|
||||
counterReset
|
||||
cursor
|
||||
direction
|
||||
display
|
||||
emptyCells
|
||||
filter
|
||||
flex
|
||||
flexBasis
|
||||
flexDirection
|
||||
flexFlow
|
||||
flexGrow
|
||||
flexShrink
|
||||
flexWrap
|
||||
cssFloat
|
||||
font
|
||||
fontFamily
|
||||
fontSize
|
||||
fontSizeAdjust
|
||||
fontStretch
|
||||
fontStyle
|
||||
fontVariant
|
||||
fontWeight
|
||||
hangingPunctuation
|
||||
height
|
||||
hyphens
|
||||
icon
|
||||
imageOrientation
|
||||
justifyContent
|
||||
left
|
||||
letterSpacing
|
||||
lineHeight
|
||||
listStyle
|
||||
listStyleImage
|
||||
listStylePosition
|
||||
listStyleType
|
||||
margin
|
||||
marginBottom
|
||||
marginLeft
|
||||
marginRight
|
||||
marginTop
|
||||
maxHeight
|
||||
maxWidth
|
||||
minHeight
|
||||
minWidth
|
||||
navDown
|
||||
navIndex
|
||||
navLeft
|
||||
navRight
|
||||
navUp
|
||||
opacity
|
||||
order
|
||||
orphans
|
||||
outline
|
||||
outlineColor
|
||||
outlineOffset
|
||||
outlineStyle
|
||||
outlineWidth
|
||||
overflow
|
||||
overflowX
|
||||
overflowY
|
||||
padding
|
||||
paddingBottom
|
||||
paddingLeft
|
||||
paddingRight
|
||||
paddingTop
|
||||
pageBreakAfter
|
||||
pageBreakBefore
|
||||
pageBreakInside
|
||||
perspective
|
||||
perspectiveOrigin
|
||||
pointerEvents
|
||||
position
|
||||
quotes
|
||||
resize
|
||||
right
|
||||
scrollbar3dLightColor # note: missing in w3schools reference
|
||||
scrollbarArrowColor # note: missing in w3schools reference
|
||||
scrollbarBaseColor # note: missing in w3schools reference
|
||||
scrollbarDarkshadowColor # note: missing in w3schools reference
|
||||
scrollbarFaceColor # note: missing in w3schools reference
|
||||
scrollbarHighlightColor # note: missing in w3schools reference
|
||||
scrollbarShadowColor # note: missing in w3schools reference
|
||||
scrollbarTrackColor # note: missing in w3schools reference
|
||||
tableLayout
|
||||
tabSize
|
||||
textAlign
|
||||
textAlignLast
|
||||
textDecoration
|
||||
textDecorationColor
|
||||
textDecorationLine
|
||||
textDecorationStyle
|
||||
textIndent
|
||||
textJustify
|
||||
textOverflow
|
||||
textShadow
|
||||
textTransform
|
||||
top
|
||||
transform
|
||||
transformOrigin
|
||||
transformStyle
|
||||
transition
|
||||
transitionDelay
|
||||
transitionDuration
|
||||
transitionProperty
|
||||
transitionTimingFunction
|
||||
unicodeBidi
|
||||
userSelect
|
||||
verticalAlign
|
||||
visibility
|
||||
whiteSpace
|
||||
width
|
||||
wordBreak
|
||||
wordSpacing
|
||||
wordWrap
|
||||
widows
|
||||
zIndex
|
||||
|
||||
macro buildStyleLookupTable(): untyped =
|
||||
var e = newTree(nnkBracket)
|
||||
for i in low(StyleAttr)..high(StyleAttr):
|
||||
e.add(newCall("kstring", newLit($i)))
|
||||
template tmpl(e) {.dirty.} =
|
||||
const
|
||||
toStyleAttrName: array[StyleAttr, kstring] = e
|
||||
result = getAst tmpl(e)
|
||||
|
||||
buildStyleLookupTable()
|
||||
|
||||
type
|
||||
StyleObj {.importc.} = object
|
||||
background*: cstring
|
||||
backgroundAttachment*: cstring
|
||||
backgroundColor*: cstring
|
||||
backgroundImage*: cstring
|
||||
backgroundPosition*: cstring
|
||||
backgroundRepeat*: cstring
|
||||
border*: cstring
|
||||
borderBottom*: cstring
|
||||
borderBottomColor*: cstring
|
||||
borderBottomStyle*: cstring
|
||||
borderBottomWidth*: cstring
|
||||
borderColor*: cstring
|
||||
borderLeft*: cstring
|
||||
borderLeftColor*: cstring
|
||||
borderLeftStyle*: cstring
|
||||
borderLeftWidth*: cstring
|
||||
borderRight*: cstring
|
||||
borderRightColor*: cstring
|
||||
borderRightStyle*: cstring
|
||||
borderRightWidth*: cstring
|
||||
borderStyle*: cstring
|
||||
borderTop*: cstring
|
||||
borderTopColor*: cstring
|
||||
borderTopStyle*: cstring
|
||||
borderTopWidth*: cstring
|
||||
borderWidth*: cstring
|
||||
bottom*: cstring
|
||||
captionSide*: cstring
|
||||
clear*: cstring
|
||||
clip*: cstring
|
||||
color*: cstring
|
||||
cursor*: cstring
|
||||
direction*: cstring
|
||||
display*: cstring
|
||||
emptyCells*: cstring
|
||||
cssFloat*: cstring
|
||||
font*: cstring
|
||||
fontFamily*: cstring
|
||||
fontSize*: cstring
|
||||
fontStretch*: cstring
|
||||
fontStyle*: cstring
|
||||
fontVariant*: cstring
|
||||
fontWeight*: cstring
|
||||
height*: cstring
|
||||
left*: cstring
|
||||
letterSpacing*: cstring
|
||||
lineHeight*: cstring
|
||||
listStyle*: cstring
|
||||
listStyleImage*: cstring
|
||||
listStylePosition*: cstring
|
||||
listStyleType*: cstring
|
||||
margin*: cstring
|
||||
marginBottom*: cstring
|
||||
marginLeft*: cstring
|
||||
marginRight*: cstring
|
||||
marginTop*: cstring
|
||||
maxHeight*: cstring
|
||||
maxWidth*: cstring
|
||||
minHeight*: cstring
|
||||
minWidth*: cstring
|
||||
overflow*: cstring
|
||||
padding*: cstring
|
||||
paddingBottom*: cstring
|
||||
paddingLeft*: cstring
|
||||
paddingRight*: cstring
|
||||
paddingTop*: cstring
|
||||
pageBreakAfter*: cstring
|
||||
pageBreakBefore*: cstring
|
||||
pointerEvents*: cstring
|
||||
position*: cstring
|
||||
right*: cstring
|
||||
scrollbar3dLightColor*: cstring
|
||||
scrollbarArrowColor*: cstring
|
||||
scrollbarBaseColor*: cstring
|
||||
scrollbarDarkshadowColor*: cstring
|
||||
scrollbarFaceColor*: cstring
|
||||
scrollbarHighlightColor*: cstring
|
||||
scrollbarShadowColor*: cstring
|
||||
scrollbarTrackColor*: cstring
|
||||
tableLayout*: cstring
|
||||
textAlign*: cstring
|
||||
textDecoration*: cstring
|
||||
textIndent*: cstring
|
||||
textTransform*: cstring
|
||||
transform*: cstring
|
||||
top*: cstring
|
||||
verticalAlign*: cstring
|
||||
visibility*: cstring
|
||||
width*: cstring
|
||||
wordSpacing*: cstring
|
||||
zIndex*: int
|
||||
Style* = ref StyleObj
|
||||
|
||||
ElementObj {.importc.} = object
|
||||
id*, class*, value*: cstring
|
||||
disabled*: bool
|
||||
style*: Style
|
||||
Element* = ref ElementObj
|
||||
|
||||
DocumentObj {.importc.} = object
|
||||
Document = ref DocumentObj
|
||||
|
||||
proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
|
||||
|
||||
proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) =
|
||||
for x in pairs:
|
||||
e.style.setStyle(toStyleAttrName[x[0]], x[1])
|
||||
|
||||
# ------- Tree manipulation ---------------------------
|
||||
|
||||
var document {.importc.}: Document
|
||||
|
||||
proc parent*(x: Element): Element {.importcpp: "#.parentNode".}
|
||||
|
||||
proc up*(x: Element; className: cstring): Element =
|
||||
result = x
|
||||
while result != nil and result.class != className:
|
||||
result = result.parent
|
||||
|
||||
proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
|
||||
proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
|
||||
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
|
||||
proc add*(n, child: Element) {.importcpp: "appendChild".}
|
||||
|
||||
proc removeChild(n, child: Element) {.importcpp.}
|
||||
proc replaceChild(n, newNode, oldNode: Element) {.importcpp.}
|
||||
|
||||
proc replace*(self, by: Element) = replaceChild(self.parent, by, self)
|
||||
proc delete*(self: Element) = removeChild(self.parent, self)
|
||||
|
||||
proc insertBefore(n, newNode, before: Element) {.importcpp.}
|
||||
proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before)
|
||||
|
||||
proc createTextNode(d: Document, text: cstring): Element {.importcpp.}
|
||||
proc text*(s: kstring): Element = createTextNode(document, s)
|
||||
|
||||
iterator items*(n: Element): Element =
|
||||
for i in 0..<n.len: yield n[i]
|
||||
|
||||
proc createElement(d: Document, identifier: cstring): Element {.importcpp.}
|
||||
proc setAttr*(n: Element; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
|
||||
|
||||
proc tree*(kind: Tag; kids: varargs[Element]): Element =
|
||||
result = createElement(document, toTagName[kind])
|
||||
for k in kids: result.add k
|
||||
|
||||
proc tree*(kind: Tag; attrs: openarray[(kstring, kstring)];
|
||||
kids: varargs[Element]): Element =
|
||||
result = tree(kind, kids)
|
||||
for a in attrs: result.setAttr(a[0], a[1])
|
||||
|
||||
# other arbitrary stuff belonging to Element
|
||||
proc focus*(e: Element) {.importcpp.}
|
||||
proc scrollIntoView*(e: Element) {.importcpp.}
|
||||
proc blur*(e: Element) {.importcpp.}
|
||||
|
||||
proc toChecked*(checked: bool): cstring =
|
||||
(if checked: cstring"checked" else: cstring(nil))
|
||||
|
||||
proc toDisabled*(disabled: bool): cstring =
|
||||
(if disabled: cstring"disabled" else: cstring(nil))
|
||||
|
||||
# -------------- Timer handling --------------------
|
||||
|
||||
type
|
||||
Timeout* {.importc.} = ref object of RootObj
|
||||
|
||||
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
|
||||
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
|
||||
|
||||
# -------------- Event handling --------------------
|
||||
|
||||
type
|
||||
EventObj {.importc.} = object
|
||||
target*: Element
|
||||
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
|
||||
code*: cstring
|
||||
isComposing*: bool
|
||||
key*: cstring
|
||||
keyCode*: int
|
||||
location*: int
|
||||
Event* = ref EventObj
|
||||
|
||||
EventHandler* = proc (ev: Event) {.closure.}
|
||||
|
||||
proc addEventListener(e: Element, ev: cstring, cb: proc(ev: Event),
|
||||
useCapture = false) {.importcpp.}
|
||||
|
||||
proc addEventHandler*(e: Element; k: EventKind; action: EventHandler) =
|
||||
case k
|
||||
of EventKind.onkeyuplater:
|
||||
proc laterWrapper(): EventHandler =
|
||||
let action = action
|
||||
var timer: Timeout
|
||||
result = proc (ev: Event) =
|
||||
proc wrapper() = action(ev)
|
||||
if timer != nil: clearTimeout(timer)
|
||||
timer = setTimeout(wrapper, 400)
|
||||
|
||||
e.addEventListener("keyup", laterWrapper())
|
||||
of EventKind.onkeyupenter:
|
||||
proc enterWrapper(): EventHandler =
|
||||
let action = action
|
||||
result = proc (ev: Event) =
|
||||
if ev.keyCode == 13: action(ev)
|
||||
|
||||
e.addEventListener("keyup", enterWrapper())
|
||||
else:
|
||||
e.addEventListener(toEventName[k], action)
|
||||
|
||||
proc addEventHandler*(e: Element; k: EventKind; action: proc()) =
|
||||
addEventHandler e, k, (proc (ev: Event) = action())
|
||||
|
||||
proc prepareDragData*(ev: Event; datatype, data: cstring)
|
||||
{.importcpp: "#.dataTransfer.setData(@)".}
|
||||
|
||||
proc recvDragData*(ev: Event; datatype: cstring): cstring
|
||||
{.importcpp: "#.dataTransfer.getData(@)".}
|
||||
|
||||
proc preventDefault*(ev: Event) {.importcpp.}
|
||||
|
||||
# ------------------ Init handling -----------------------------------
|
||||
|
||||
proc replaceById*(newTree: Element; id: cstring = "ROOT") =
|
||||
let x = getElementById(id)
|
||||
x.parent.replaceChild(newTree, x)
|
||||
newTree.id = id
|
||||
|
||||
proc setWindowOnload(h: EventHandler) {.importcpp: "window.onload = #".}
|
||||
proc setInitializer*(initializer: proc (hashPart: kstring): Element;
|
||||
root: cstring = "ROOT") =
|
||||
var onhashChange {.importc: "window.onhashchange".}: proc()
|
||||
var hashPart {.importc: "window.location.hash".}: cstring
|
||||
|
||||
setWindowOnload proc (ev: Event) =
|
||||
replaceById root, initializer(hashPart)
|
||||
onhashchange = proc () =
|
||||
replaceById root, initializer(hashPart)
|
||||
|
||||
# ------------------ DSL section -------------------------------------
|
||||
|
||||
const
|
||||
StmtContext = ["inc", "echo", "dec", "!"]
|
||||
SpecialAttrs = ["id", "class", "value"]
|
||||
|
||||
type
|
||||
ComponentKind {.pure.} = enum
|
||||
None,
|
||||
Tag
|
||||
|
||||
var
|
||||
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
|
||||
|
||||
proc isComponent(x: string): ComponentKind {.compileTime.} =
|
||||
allcomponents.getOrDefault(x)
|
||||
|
||||
proc addTags() {.compileTime.} =
|
||||
let x = (bindSym"Tag").getTypeImpl
|
||||
expectKind(x, nnkEnumTy)
|
||||
for i in ord(Tag.html)..ord(Tag.high):
|
||||
# +1 because of empty node at the start of the enum AST:
|
||||
let tag = $x[i+1]
|
||||
allcomponents[tag] = ComponentKind.Tag
|
||||
|
||||
static:
|
||||
addTags()
|
||||
|
||||
|
||||
proc getName(n: NimNode): string =
|
||||
case n.kind
|
||||
of nnkIdent:
|
||||
result = $n.ident
|
||||
of nnkAccQuoted:
|
||||
result = ""
|
||||
for i in 0..<n.len:
|
||||
result.add getName(n[i])
|
||||
of nnkStrLit..nnkTripleStrLit:
|
||||
result = n.strVal
|
||||
else:
|
||||
#echo repr n
|
||||
expectKind(n, nnkIdent)
|
||||
|
||||
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
|
||||
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
|
||||
|
||||
proc toKstring(n: NimNode): NimNode =
|
||||
if n.kind == nnkStrLit:
|
||||
result = newCall(bindSym"kstring", n)
|
||||
else:
|
||||
result = copyNimNode(n)
|
||||
for child in n:
|
||||
result.add toKstring(child)
|
||||
|
||||
proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||
# we need to distinguish statement and expression contexts:
|
||||
# every call statement 's' needs to be transformed to 'dest.add s'.
|
||||
# If expressions need to be distinguished from if statements. Since
|
||||
# we know we start in a statement context, it's pretty simple to
|
||||
# figure out expression contexts: In calls everything is an expression
|
||||
# (except for the last child of the macros we consider here),
|
||||
# lets, consts, types can be considered as expressions
|
||||
# case is complex, calls are assumed to produce a value.
|
||||
when defined(js):
|
||||
template evHandler(): untyped = bindSym"addEventHandler"
|
||||
else:
|
||||
template evHandler(): untyped = ident"addEventHandler"
|
||||
|
||||
case n.kind
|
||||
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
|
||||
if tmpContext != nil:
|
||||
result = newCall(bindSym"add", tmpContext, n)
|
||||
else:
|
||||
result = n
|
||||
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
|
||||
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
|
||||
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
|
||||
# recurse for the last son:
|
||||
result = copyNimTree(n)
|
||||
let L = n.len
|
||||
assert n.len == result.len
|
||||
if L > 0:
|
||||
result[L-1] = tcall2(result[L-1], tmpContext)
|
||||
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
|
||||
nnkFinally:
|
||||
# recurse for every child:
|
||||
result = copyNimNode(n)
|
||||
for x in n:
|
||||
result.add tcall2(x, tmpContext)
|
||||
of nnkCaseStmt:
|
||||
# recurse for children, but don't add call for case ident
|
||||
result = copyNimNode(n)
|
||||
result.add n[0]
|
||||
for i in 1 ..< n.len:
|
||||
result.add tcall2(n[i], tmpContext)
|
||||
of nnkProcDef:
|
||||
let name = getName n[0]
|
||||
if name.startsWith"on":
|
||||
# turn it into an anon proc:
|
||||
let anon = copyNimTree(n)
|
||||
anon[0] = newEmptyNode()
|
||||
if tmpContext == nil:
|
||||
error "no Element to attach the event handler to"
|
||||
else:
|
||||
result = newCall(evHandler(), tmpContext,
|
||||
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
|
||||
else:
|
||||
result = n
|
||||
of nnkVarSection, nnkLetSection, nnkConstSection:
|
||||
result = n
|
||||
of nnkCallKinds - {nnkInfix}:
|
||||
let op = getName(n[0])
|
||||
if isComponent(op) == ComponentKind.Tag:
|
||||
let tmp = genSym(nskLet, "tmp")
|
||||
let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0]))
|
||||
result = newTree(
|
||||
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
|
||||
newLetStmt(tmp, call))
|
||||
for i in 1 ..< n.len:
|
||||
# named parameters are transformed into attributes or events:
|
||||
let x = n[i]
|
||||
if x.kind == nnkExprEqExpr:
|
||||
let key = getName x[0]
|
||||
if key.startsWith("on"):
|
||||
result.add newCall(evHandler(),
|
||||
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
|
||||
elif key in SpecialAttrs:
|
||||
result.add newDotAsgn(tmp, key, x[1])
|
||||
elif eqIdent(key, "style"):
|
||||
result.add newCall(bindSym"applyStyles", tmp, toKstring x[1])
|
||||
elif eqIdent(key, "setFocus"):
|
||||
result.add newCall(bindSym"focus", tmp)
|
||||
else:
|
||||
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
|
||||
elif eqIdent(x, "setFocus"):
|
||||
result.add newCall(bindSym"focus", tmp)
|
||||
else:
|
||||
result.add tcall2(x, tmp)
|
||||
if tmpContext == nil:
|
||||
result.add tmp
|
||||
else:
|
||||
result.add newCall(bindSym"add", tmpContext, tmp)
|
||||
elif tmpContext != nil and op notin StmtContext:
|
||||
var hasEventHandlers = false
|
||||
for i in 1..<n.len:
|
||||
let it = n[i]
|
||||
if it.kind in {nnkProcDef, nnkStmtList}:
|
||||
hasEventHandlers = true
|
||||
break
|
||||
if not hasEventHandlers:
|
||||
result = newCall(bindSym"add", tmpContext, n)
|
||||
else:
|
||||
let tmp = genSym(nskLet, "tmp")
|
||||
var slicedCall = newCall(n[0])
|
||||
let ex = newTree(nnkStmtListExpr)
|
||||
ex.add newEmptyNode() # will become the let statement
|
||||
for i in 1..<n.len:
|
||||
let it = n[i]
|
||||
if it.kind in {nnkProcDef, nnkStmtList}:
|
||||
ex.add tcall2(it, tmp)
|
||||
else:
|
||||
slicedCall.add it
|
||||
ex[0] = newLetStmt(tmp, slicedCall)
|
||||
ex.add tmp
|
||||
result = newCall(bindSym"add", tmpContext, ex)
|
||||
elif op == "!" and n.len == 2:
|
||||
result = n[1]
|
||||
else:
|
||||
result = n
|
||||
else:
|
||||
result = n
|
||||
|
||||
macro buildHtml*(tag, children: untyped): Element =
|
||||
let kids = newProc(procType=nnkDo, body=children)
|
||||
expectKind kids, nnkDo
|
||||
var call: NimNode
|
||||
if tag.kind in nnkCallKinds:
|
||||
call = tag
|
||||
else:
|
||||
call = newCall(tag)
|
||||
call.add body(kids)
|
||||
result = tcall2(call, nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
|
||||
macro buildHtml*(children: untyped): Element =
|
||||
let kids = newProc(procType=nnkDo, body=children)
|
||||
expectKind kids, nnkDo
|
||||
result = tcall2(body(kids), nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
|
||||
macro flatHtml*(tag: untyped): Element =
|
||||
result = tcall2(tag, nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
151
experiments/trello/trello.nim
Normal file
151
experiments/trello/trello.nim
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
|
||||
import knete, widgets
|
||||
import karax / jstrutils
|
||||
|
||||
type
|
||||
Attachable* = ref object of RootObj ## an element that is attachable to
|
||||
## DOM elements.
|
||||
attachedTo*: seq[Element]
|
||||
|
||||
TaskId = int
|
||||
Task = ref object
|
||||
id: TaskId
|
||||
name, desc: kstring
|
||||
|
||||
ColumnId = distinct int
|
||||
Column = ref object of Attachable
|
||||
id: ColumnId
|
||||
header: kstring
|
||||
tasks: seq[Task]
|
||||
|
||||
Board = ref object
|
||||
columns: seq[Column]
|
||||
|
||||
var b = Board(columns: @[Column(id: ColumnId 0, header: "To Do"),
|
||||
Column(id: ColumnId 1, header: "Doing"),
|
||||
Column(id: ColumnId 2, header: "Done")])
|
||||
|
||||
proc removeTask(t: TaskId) =
|
||||
for c in b.columns:
|
||||
for i in 0 ..< c.tasks.len:
|
||||
if c.tasks[i].id == t:
|
||||
delete(c.tasks, i)
|
||||
break
|
||||
|
||||
proc moveTask(t: TaskId; dest: Column) =
|
||||
for c in b.columns:
|
||||
for i in 0 ..< c.tasks.len:
|
||||
let task = c.tasks[i]
|
||||
if task.id == t:
|
||||
delete(c.tasks, i)
|
||||
dest.tasks.add task
|
||||
return
|
||||
|
||||
var nextTaskId = 0 ## negative because they are not yet backed
|
||||
## up by the database
|
||||
|
||||
proc createTask(c: Column; name, desc: kstring): Task =
|
||||
dec nextTaskId
|
||||
result = Task(id: TaskId nextTaskId, name: name, desc: desc)
|
||||
c.tasks.add result
|
||||
|
||||
# --------------------- UI -----------------------------------------------
|
||||
|
||||
proc renderTask(t: Task): Element =
|
||||
proc dodelete =
|
||||
removeTask t.id
|
||||
delete result
|
||||
|
||||
proc dragstart(ev: Event) =
|
||||
ev.prepareDragData("taskid", ev.target.id)
|
||||
|
||||
result = buildHtml():
|
||||
tdiv(draggable="true", ondragstart=dragstart, id = &t.id):
|
||||
bindField t.name
|
||||
br()
|
||||
bindField t.desc
|
||||
button(onclick = dodelete):
|
||||
text cross
|
||||
|
||||
type
|
||||
NewTaskPanel = object
|
||||
c: Column
|
||||
e: Element
|
||||
|
||||
proc open(p: var NewTaskPanel; c: Column) =
|
||||
p.e.style.display = "block"
|
||||
p.c = c
|
||||
|
||||
proc newTaskDialog(): NewTaskPanel =
|
||||
var nameInp = buildHtml():
|
||||
input(setFocus = true)
|
||||
var descInp = buildHtml():
|
||||
input()
|
||||
|
||||
proc close =
|
||||
result.e.style.display = "none"
|
||||
|
||||
proc submit =
|
||||
let t = createTask(result.c, nameInp.value, descInp.value)
|
||||
close()
|
||||
result.c.attachedTo[0].add renderTask(t)
|
||||
|
||||
result.e = buildHtml():
|
||||
tdiv(style={display: "none", position: "fixed",
|
||||
left: "0", top: "0", width: "100%", height: "100%",
|
||||
overflow: "auto",
|
||||
backgroundColor: "rgb(0,0,0)",
|
||||
backgroundColor: "rgba(0,0,0,0.4)", zIndex: "1"}):
|
||||
tdiv(style={backgroundColor: "#fefefe",
|
||||
margin: "15% auto", padding: "20px", border: "1px solid #888",
|
||||
width: "80%"}):
|
||||
span(onclick = close, style={color: "#aaa",
|
||||
cssFloat: "right",
|
||||
fontSize: "28px",
|
||||
fontWeight: "bold"}):
|
||||
text cross
|
||||
p:
|
||||
text "Task name"
|
||||
nameInp
|
||||
p:
|
||||
text "Task description"
|
||||
descInp
|
||||
span(onclick = submit, style={color: "#0f0",
|
||||
cssFloat: "left",
|
||||
fontWeight: "bold"}):
|
||||
text "Submit"
|
||||
|
||||
var dialog = newTaskDialog()
|
||||
|
||||
proc renderColumn(c: Column): Element =
|
||||
proc doadd(c: Column): proc () =
|
||||
result = proc() = dialog.open(c)
|
||||
|
||||
proc allowDrop(ev: Event) = ev.preventDefault()
|
||||
proc drop(ev: Event) =
|
||||
ev.preventDefault()
|
||||
let data = ev.recvDragData("taskid")
|
||||
moveTask(parseInt data, c)
|
||||
ev.target.up("mycolumn").add(getElementById(data))
|
||||
|
||||
result = buildHtml():
|
||||
tdiv(class = "mycolumn", style = {cssFloat: "left", width: "20%"},
|
||||
ondrop = drop, ondragover = allowDrop):
|
||||
tdiv(class = "myheader"):
|
||||
bindField c.header
|
||||
span(onclick = doadd(c)):
|
||||
text plus
|
||||
for t in c.tasks:
|
||||
renderTask(t)
|
||||
c.attachedTo.add result
|
||||
|
||||
proc renderBoard(b: Board): Element =
|
||||
result = buildHtml(tdiv):
|
||||
dialog.e
|
||||
for c in b.columns:
|
||||
renderColumn(c)
|
||||
|
||||
proc main(hashPart: kstring): Element =
|
||||
result = renderBoard(b)
|
||||
|
||||
setInitializer main
|
||||
43
experiments/trello/widgets.nim
Normal file
43
experiments/trello/widgets.nim
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
## Common widget implemenations
|
||||
|
||||
import knete
|
||||
|
||||
const
|
||||
cross* = "x"
|
||||
plus* = kstring"+"
|
||||
|
||||
const
|
||||
LEFT* = 37
|
||||
UP* = 38
|
||||
RIGHT* = 39
|
||||
DOWN* = 40
|
||||
TAB* = 9
|
||||
ESC* = 27
|
||||
ENTER* = 13
|
||||
|
||||
proc editable*(x: kstring; onchanged: proc (value: kstring);
|
||||
isEdited = false): Element =
|
||||
proc onenter(ev: Event) = ev.target.blur()
|
||||
proc submit(ev: Event) =
|
||||
onchanged(ev.target.value)
|
||||
# close the loop: This is a common pattern in Knete.
|
||||
replace(result, editable(ev.target.value, onchanged, false))
|
||||
|
||||
proc makeEditable() =
|
||||
# close the loop: This is a common pattern in Knete.
|
||||
replace(result, editable(x, onchanged, true))
|
||||
|
||||
result = buildHtml():
|
||||
if isEdited:
|
||||
input(class = "edit",
|
||||
onblur = submit,
|
||||
onkeyupenter = onenter,
|
||||
value = x,
|
||||
setFocus = true)
|
||||
else:
|
||||
span(onclick = makeEditable):
|
||||
text x
|
||||
|
||||
template bindField*(field): Element =
|
||||
editable field, proc (value: kstring) =
|
||||
field = value
|
||||
12
karax.nimble
12
karax.nimble
|
|
@ -1,12 +1,16 @@
|
|||
# Package
|
||||
|
||||
version = "0.1.0"
|
||||
author = "Araq"
|
||||
version = "1.2.1"
|
||||
author = "Andreas Rumpf"
|
||||
description = "Karax is a framework for developing single page applications in Nim."
|
||||
license = "MIT"
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires "nim >= 0.16.1"
|
||||
requires "nim >= 0.18.0"
|
||||
requires "ws"
|
||||
requires "dotenv"
|
||||
skipDirs = @["examples", "experiments", "tests"]
|
||||
|
||||
skipDirs = @["examples/todoapp"]
|
||||
bin = @["karax/tools/karun"]
|
||||
installExt = @["nim"]
|
||||
|
|
|
|||
160
karax/autocomplete.nim
Normal file
160
karax/autocomplete.nim
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
|
||||
import karax, karaxdsl, vdom, kdom, jstrutils
|
||||
|
||||
type
|
||||
AutocompleteState* = ref object
|
||||
choices, candidates: seq[cstring]
|
||||
selected, maxMatches: int
|
||||
showCandidates, controlPressed: bool
|
||||
|
||||
proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState =
|
||||
## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum
|
||||
## number of elements to show.
|
||||
AutocompleteState(choices: choices, candidates: @[],
|
||||
selected: -1, maxMatches: maxMatches, showCandidates: false,
|
||||
controlPressed: false)
|
||||
|
||||
proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode =
|
||||
var inp: VNode
|
||||
|
||||
proc commit(ev: Event) =
|
||||
s.showCandidates = false
|
||||
onselection(inp.dom.value)
|
||||
when false:
|
||||
if inp.dom != nil:
|
||||
echo "setting to A ", inp.dom.value.isNil
|
||||
result.text = inp.dom.value
|
||||
else:
|
||||
echo "setting to B ", inp.text.isNil
|
||||
result.text = inp.text
|
||||
for e in result.events:
|
||||
if e[0] == EventKind.onchange:
|
||||
e[1](ev, result)
|
||||
|
||||
proc onkeyuplater(ev: Event; n: VNode) =
|
||||
if not s.controlPressed:
|
||||
let v = n.value
|
||||
s.candidates.setLen 0
|
||||
for c in s.choices:
|
||||
if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c)
|
||||
|
||||
proc onkeydown(ev: Event; n: VNode) =
|
||||
const
|
||||
LEFT = 37
|
||||
UP = 38
|
||||
RIGHT = 39
|
||||
DOWN = 40
|
||||
TAB = 9
|
||||
ESC = 27
|
||||
ENTER = 13
|
||||
# UP: Move focus to the previous item. If on first item, move focus to the input.
|
||||
# If on the input, move focus to last item.
|
||||
# DOWN: Move focus to the next item. If on last item, move focus to the input.
|
||||
# If on the input, move focus to the first item.
|
||||
# ESCAPE: Close the menu.
|
||||
# ENTER: Select the currently focused item and close the menu.
|
||||
# TAB: Select the currently focused item, close the menu, and
|
||||
# move focus to the next focusable element
|
||||
s.controlPressed = false
|
||||
case ev.keyCode
|
||||
of UP:
|
||||
s.controlPressed = true
|
||||
s.showCandidates = true
|
||||
if s.selected > 0:
|
||||
dec s.selected
|
||||
n.setInputText s.candidates[s.selected]
|
||||
of DOWN:
|
||||
s.controlPressed = true
|
||||
s.showCandidates = true
|
||||
if s.selected < s.candidates.len - 1:
|
||||
inc s.selected
|
||||
n.setInputText s.candidates[s.selected]
|
||||
of ESC:
|
||||
s.showCandidates = false
|
||||
s.controlPressed = true
|
||||
of ENTER:
|
||||
s.controlPressed = true
|
||||
# inp.setInputText s.choices[i]
|
||||
commit(ev)
|
||||
else:
|
||||
discard
|
||||
|
||||
proc window(s: AutocompleteState): (int, int) =
|
||||
var first, last: int
|
||||
if s.selected >= 0:
|
||||
first = s.selected - (s.maxMatches div 2)
|
||||
last = s.selected + (s.maxMatches div 2)
|
||||
if first < 0: first = 0
|
||||
# too few because we had to trim first?
|
||||
if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1
|
||||
else:
|
||||
first = 0
|
||||
last = s.maxMatches - 1
|
||||
if last > high(s.candidates): last = high(s.candidates)
|
||||
# still too few because we're at the end?
|
||||
if (last - first + 1) < s.maxMatches:
|
||||
first = last - s.maxMatches + 1
|
||||
if first < 0: first = 0
|
||||
|
||||
result = (first, last)
|
||||
|
||||
inp = buildHtml:
|
||||
input(onkeyuplater = onkeyuplater,
|
||||
onkeydown = onkeydown,
|
||||
# onblur = proc (ev: Event; n: VNode) = commit(ev),
|
||||
onfocus = proc (ev: Event; n: VNode) =
|
||||
onkeyuplater(ev, n)
|
||||
s.showCandidates = true)
|
||||
|
||||
proc select(i: int): proc(ev: Event; n: VNode) =
|
||||
result = proc(ev: Event; n: VNode) =
|
||||
s.selected = i
|
||||
s.showCandidates = false
|
||||
inp.setInputText s.choices[i]
|
||||
commit(ev)
|
||||
|
||||
result = buildHtml(table):
|
||||
tr:
|
||||
td:
|
||||
inp
|
||||
if s.showCandidates:
|
||||
let (first, last) = window(s)
|
||||
for i in first..last:
|
||||
tr:
|
||||
td(onclick = select(i), class = cstring"button " &
|
||||
(if i == s.selected: cstring"is-primary" else: "")):
|
||||
text s.candidates[i]
|
||||
|
||||
when isMainModule:
|
||||
const suggestions = @[cstring"ActionScript",
|
||||
"AppleScript",
|
||||
"Asp",
|
||||
"BASIC",
|
||||
"C",
|
||||
"C++",
|
||||
"Clojure",
|
||||
"COBOL",
|
||||
"Erlang",
|
||||
"Fortran",
|
||||
"Groovy",
|
||||
"Haskell",
|
||||
"Java",
|
||||
"JavaScript",
|
||||
"Lisp",
|
||||
"Nim",
|
||||
"Perl",
|
||||
"PHP",
|
||||
"Python",
|
||||
"Ruby",
|
||||
"Scala",
|
||||
"Scheme"]
|
||||
|
||||
var s = newAutocomplete(suggestions)
|
||||
|
||||
proc main(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
autocomplete(s, proc (s: cstring) = echo "now ", s)#:
|
||||
# proc onchange(ev: Event; n: VNode) =
|
||||
# echo "now selected ", n.kind
|
||||
|
||||
setRenderer(main)
|
||||
114
karax/compact.nim
Normal file
114
karax/compact.nim
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
## Components in Karax are built by the ``.component`` macro annotation.
|
||||
|
||||
when defined(js):
|
||||
import jdict, kdom
|
||||
|
||||
import macros, vdom, tables, strutils, kbase
|
||||
|
||||
when defined(js):
|
||||
var
|
||||
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
|
||||
else:
|
||||
var
|
||||
vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]()
|
||||
|
||||
type
|
||||
ComponentKind* {.pure.} = enum
|
||||
None,
|
||||
Tag,
|
||||
VNode
|
||||
|
||||
var
|
||||
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
|
||||
|
||||
proc isComponent*(x: string): ComponentKind {.compileTime.} =
|
||||
allcomponents.getOrDefault(x)
|
||||
|
||||
proc addTags() {.compileTime.} =
|
||||
let x = (bindSym"VNodeKind").getTypeImpl
|
||||
expectKind(x, nnkEnumTy)
|
||||
for i in ord(VNodeKind.html)..ord(VNodeKind.high):
|
||||
# +1 because of empty node at the start of the enum AST:
|
||||
let tag = $x[i+1]
|
||||
allcomponents[tag] = ComponentKind.Tag
|
||||
|
||||
static:
|
||||
addTags()
|
||||
|
||||
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
|
||||
#let t = symbolicType.getTypeImpl
|
||||
let t = repr(symbolicType)
|
||||
case t
|
||||
of "cstring":
|
||||
result = quote do:
|
||||
args[`index`].text
|
||||
of "int", "VKey":
|
||||
result = quote do:
|
||||
args[`index`].intValue
|
||||
of "bool":
|
||||
result = quote do:
|
||||
args[`index`].intValue != 0
|
||||
elif t.endsWith"Kind":
|
||||
result = quote do:
|
||||
`symbolicType`(args[`index`].intValue)
|
||||
else:
|
||||
# just pass it along, maybe there is some conversion for it:
|
||||
result = quote do:
|
||||
args[`index`]
|
||||
|
||||
proc newname*(n: NimNode): NimNode =
|
||||
if n.kind == nnkPostfix:
|
||||
n[1] = newname(n[1])
|
||||
result = n
|
||||
elif n.kind == nnkSym:
|
||||
result = ident(n.strVal)
|
||||
else:
|
||||
result = n
|
||||
|
||||
when defined(js):
|
||||
macro compact*(prc: untyped): untyped =
|
||||
## A 'compact' tree generation proc is one that only depends on its
|
||||
## inputs and should be stored as a compact virtual DOM tree and
|
||||
## only expanded on demand (when its inputs changed).
|
||||
var n = prc.copyNimNode
|
||||
for i in 0..6: n.add prc[i].copyNimTree
|
||||
expectKind(n, nnkProcDef)
|
||||
if n[0].kind == nnkEmpty:
|
||||
error("please pass a non anonymous proc", n[0])
|
||||
let name = n[0]
|
||||
let params = params(n)
|
||||
let rettype = repr params[0]
|
||||
var isvirtual = ComponentKind.None
|
||||
if rettype == "VNode":
|
||||
isvirtual = ComponentKind.VNode
|
||||
else:
|
||||
error "component must return VNode", params[0]
|
||||
let realName = if name.kind == nnkPostfix: name[1] else: name
|
||||
let nn = $realName
|
||||
n[0] = ident("inner" & nn)
|
||||
var unpackCall = newCall(n[0])
|
||||
var counter = 0
|
||||
for i in 1 ..< params.len:
|
||||
let param = params[i]
|
||||
let L = param.len
|
||||
let typ = param[L-2]
|
||||
for j in 0 .. L-3:
|
||||
unpackCall.add unpack(typ, counter)
|
||||
inc counter
|
||||
|
||||
template vwrapper(pname, unpackCall) {.dirty.} =
|
||||
proc pname(args: seq[VNode]): VNode =
|
||||
unpackCall
|
||||
|
||||
template vregister(key, val) =
|
||||
bind jdict.`[]=`
|
||||
`[]=`(vcomponents, kstring(key), val)
|
||||
|
||||
result = newTree(nnkStmtList, n)
|
||||
|
||||
if isvirtual == ComponentKind.VNode:
|
||||
result.add getAst(vwrapper(newname name, unpackCall))
|
||||
result.add getAst(vregister(newLit(nn), realName))
|
||||
allcomponents[nn] = isvirtual
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
32
karax/errors.nim
Normal file
32
karax/errors.nim
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
## Error handling logic for form input validation.
|
||||
|
||||
import karax, jdict
|
||||
|
||||
var
|
||||
gerrorMsgs = newJDict[cstring, cstring]()
|
||||
gerrorCounter = 0
|
||||
|
||||
proc hasErrors*(): bool = gerrorCounter != 0
|
||||
|
||||
proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0
|
||||
|
||||
proc disableOnError*(): cstring = toDisabled(hasErrors())
|
||||
|
||||
proc getError*(field: cstring): cstring =
|
||||
if not gerrorMsgs.contains(field):
|
||||
result = ""
|
||||
else:
|
||||
result = gerrorMsgs[field]
|
||||
|
||||
proc setError*(field, msg: cstring) =
|
||||
let previous = getError(field)
|
||||
if len(msg) == 0:
|
||||
if len(previous) != 0: dec(gerrorCounter)
|
||||
else:
|
||||
if len(previous) == 0: inc(gerrorCounter)
|
||||
gerrorMsgs[field] = msg
|
||||
|
||||
proc clearErrors*() =
|
||||
let m = gerrorMsgs
|
||||
for k in m.keys:
|
||||
setError(k, "")
|
||||
152
karax/i18n.nim
Normal file
152
karax/i18n.nim
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
## Karax -- Single page applications for Nim.
|
||||
|
||||
## i18n support for Karax applications. We distinguish between:
|
||||
##
|
||||
## - translate: Lookup text translations
|
||||
## - localize: Localize Date and Time objects to local formats
|
||||
##
|
||||
## Localization has not yet been implemented.
|
||||
|
||||
import languages, jdict, jstrutils
|
||||
|
||||
var currentLanguage = Language.enUS
|
||||
|
||||
proc getLanguage(): cstring =
|
||||
{.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".}
|
||||
|
||||
proc detectLanguage*(): Language =
|
||||
let x = getLanguage()
|
||||
for i in low(Language)..high(Language):
|
||||
if languageToCode[i] == x: return i
|
||||
return Language.enUS
|
||||
|
||||
proc setCurrentLanguage*(newLanguage = detectLanguage()) =
|
||||
currentLanguage = newLanguage
|
||||
|
||||
proc getCurrentLanguage*(): Language = currentLanguage
|
||||
|
||||
type Translation* = JDict[cstring, cstring]
|
||||
|
||||
var
|
||||
translations: array[Language, Translation]
|
||||
|
||||
proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t
|
||||
|
||||
proc addTranslation*(lang: Language; key, val: cstring) =
|
||||
if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]()
|
||||
translations[lang][key] = val
|
||||
|
||||
proc translate(x: cstring): cstring =
|
||||
let y = translations[currentLanguage]
|
||||
if y != nil and y.contains(x):
|
||||
result = y[x]
|
||||
else:
|
||||
result = x
|
||||
|
||||
type TranslatedString* = distinct cstring
|
||||
|
||||
template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x))
|
||||
|
||||
proc raiseInvalidFormat(errmsg: string) =
|
||||
raise newException(ValueError, errmsg)
|
||||
|
||||
discard """
|
||||
$[1:item|items]1$ selected.
|
||||
"""
|
||||
|
||||
proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".}
|
||||
proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".}
|
||||
|
||||
proc parseChoice(f: cstring; i, choice: int,
|
||||
r: JSeq[char]) =
|
||||
var i = i
|
||||
while i < f.len:
|
||||
var n = 0
|
||||
let oldI = i
|
||||
var toAdd = false
|
||||
while i < f.len and f{i} >= '0' and f{i} <= '9':
|
||||
n = n * 10 + ord(f{i}) - ord('0')
|
||||
inc i
|
||||
if oldI != i:
|
||||
if f{i} == ':':
|
||||
inc i
|
||||
else:
|
||||
raiseInvalidFormat"':' after number expected"
|
||||
toAdd = choice == n
|
||||
else:
|
||||
# an else section does not start with a number:
|
||||
toAdd = true
|
||||
while i < f.len and f{i} != ']' and f{i} != '|':
|
||||
if toAdd: r.sadd f{i}
|
||||
inc i
|
||||
if toAdd: break
|
||||
inc i
|
||||
|
||||
proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".}
|
||||
proc add(x: JSeq[char]; y: cstring) =
|
||||
for i in 0..<y.len: x.sadd y{i}
|
||||
|
||||
proc `%`*(formatString: TranslatedString; args: openArray[cstring]): cstring =
|
||||
let f = cstring(formatString)
|
||||
var i = 0
|
||||
var num = 0
|
||||
var r = newJSeq[char]()
|
||||
while i < f.len:
|
||||
if f{i} == '$' and i+1 < f.len:
|
||||
inc i
|
||||
case f{i}
|
||||
of '#':
|
||||
r.add args[num]
|
||||
inc i
|
||||
inc num
|
||||
of '1'..'9', '-':
|
||||
var j = 0
|
||||
var negative = f{i} == '-'
|
||||
if negative: inc i
|
||||
while f{i} >= '0' and f{i} <= '9':
|
||||
j = j * 10 + ord(f{i}) - ord('0')
|
||||
inc i
|
||||
let idx = if not negative: j-1 else: args.len-j
|
||||
r.add args[idx]
|
||||
of '$':
|
||||
inc(i)
|
||||
r.add '$'
|
||||
of '[':
|
||||
let start = i+1
|
||||
while i < f.len and f{i} != ']': inc i
|
||||
inc i
|
||||
if i >= f.len: raiseInvalidFormat"']' expected"
|
||||
case f{i}
|
||||
of '#':
|
||||
parseChoice(f, start, parseInt args[num], r)
|
||||
inc i
|
||||
inc num
|
||||
of '1'..'9', '-':
|
||||
var j = 0
|
||||
var negative = f{i} == '-'
|
||||
if negative: inc i
|
||||
while f{i} >= '0' and f{i} <= '9':
|
||||
j = j * 10 + ord(f{i}) - ord('0')
|
||||
inc i
|
||||
let idx = if not negative: j-1 else: args.len-j
|
||||
parseChoice(f, start, parseInt args[idx], r)
|
||||
else: raiseInvalidFormat"argument index expected after ']'"
|
||||
else:
|
||||
raiseInvalidFormat("'#', '$', or number expected")
|
||||
if i < f.len and f{i} == '$': inc i
|
||||
else:
|
||||
r.sadd f{i}
|
||||
inc i
|
||||
result = join(r)
|
||||
|
||||
|
||||
when isMainModule:
|
||||
addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1")
|
||||
setCurrentLanguage(Language.deDE)
|
||||
|
||||
echo(i18n"$#$ -> $#$" % [cstring"1", "2"])
|
||||
echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"])
|
||||
echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"])
|
||||
echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"])
|
||||
echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"])
|
||||
echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"])
|
||||
30
karax/jdict.nim
Normal file
30
karax/jdict.nim
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
type
|
||||
JDict*[K, V] = ref object
|
||||
|
||||
proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
|
||||
proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
|
||||
|
||||
proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
|
||||
|
||||
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
|
||||
|
||||
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
|
||||
|
||||
iterator keys*[K, V](d: JDict[K, V]): K =
|
||||
var kkk: K
|
||||
{.emit: ["for (", kkk, " in ", d, ") {"].}
|
||||
yield kkk
|
||||
{.emit: ["}"].}
|
||||
|
||||
type
|
||||
JSeq*[T] = ref object
|
||||
|
||||
proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.}
|
||||
proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.}
|
||||
|
||||
proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".}
|
||||
proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.}
|
||||
proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.}
|
||||
|
||||
proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.}
|
||||
67
karax/jjson.nim
Normal file
67
karax/jjson.nim
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
## This module implements some small zero-overhead 'JsonNode' type
|
||||
## and helper that maps directly to JavaScript objects.
|
||||
|
||||
type
|
||||
JsonNode* {.importc.} = ref object
|
||||
|
||||
proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".}
|
||||
proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".}
|
||||
proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T)
|
||||
{.importcpp: "#[#] = #".}
|
||||
proc length(x: JsonNode): int {.importcpp: "#.length".}
|
||||
proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length)
|
||||
|
||||
proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode =
|
||||
result = JsonNode()
|
||||
for f in fields:
|
||||
result[f[0]] = f[1]
|
||||
|
||||
proc newJObject*(): JsonNode =
|
||||
result = JsonNode()
|
||||
|
||||
proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".}
|
||||
|
||||
proc newJNull*(): JsonNode = nil
|
||||
|
||||
template `%`*(x: typed): JsonNode = cast[JsonNode](x)
|
||||
template `%`*(x: string): JsonNode = cast[JsonNode](cstring x)
|
||||
|
||||
proc getNum*(x: JsonNode): int {.importcpp: "#".}
|
||||
|
||||
proc getInt*(x: JsonNode): int {.importcpp: "#".}
|
||||
proc getStr*(x: JsonNode): cstring {.importcpp: "#".}
|
||||
proc getFNum*(x: JsonNode): cstring {.importcpp: "#".}
|
||||
|
||||
iterator items*(x: JsonNode): JsonNode =
|
||||
for i in 0..<len(x): yield x[i]
|
||||
|
||||
import macros
|
||||
|
||||
proc toJson(x: NimNode): NimNode {.compiletime.} =
|
||||
case x.kind
|
||||
of nnkBracket:
|
||||
result = newCall(bindSym"newJArray")
|
||||
for i in 0 ..< x.len:
|
||||
result.add(toJson(x[i]))
|
||||
of nnkTableConstr:
|
||||
result = newCall(bindSym"newJsonNode")
|
||||
for i in 0 ..< x.len:
|
||||
x[i].expectKind nnkExprColonExpr
|
||||
let key = x[i][0]
|
||||
let a = if x[i].kind in {nnkIdent, nnkSym, nnkAccQuoted}:
|
||||
newLit($key)
|
||||
else:
|
||||
key
|
||||
result.add newTree(nnkPar, newCall(bindSym"cstring", a), toJson(x[i][1]))
|
||||
of nnkCurly:
|
||||
x.expectLen(0)
|
||||
result = newCall(bindSym"newJObject")
|
||||
of nnkNilLit:
|
||||
result = newCall(bindSym"newJNull")
|
||||
else:
|
||||
result = newCall(bindSym"%", x)
|
||||
|
||||
macro `%*`*(x: untyped): untyped =
|
||||
## Convert an expression to a JsonNode directly, without having to specify
|
||||
## `%` for every element.
|
||||
result = toJson(x)
|
||||
|
|
@ -4,8 +4,14 @@ proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
|
|||
|
||||
proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, nodecl.}
|
||||
|
||||
proc strip*(s: cstring): cstring {.importcpp: "#.trim()", nodecl.}
|
||||
|
||||
proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
|
||||
proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
|
||||
|
||||
proc containsIgnoreCase*(a, b: cstring): bool {.
|
||||
importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.}
|
||||
|
||||
proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
|
||||
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
|
||||
|
||||
|
|
@ -14,10 +20,14 @@ proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
|
|||
proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
|
||||
proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
|
||||
proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.}
|
||||
proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.}
|
||||
|
||||
proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.}
|
||||
|
||||
proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
|
||||
asm """
|
||||
return s.match(/^[0-9]+$/);
|
||||
return `s`.match(/^[0-9]+$/);
|
||||
"""
|
||||
|
||||
proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.}
|
||||
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}
|
||||
17
karax/jwebsockets.nim
Normal file
17
karax/jwebsockets.nim
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
## Websockets support for the JSON backend.
|
||||
|
||||
type
|
||||
MessageEvent* {.importc.} = ref object
|
||||
data*: cstring
|
||||
|
||||
WebSocket* {.importc.} = ref object
|
||||
onmessage*: proc (e: MessageEvent)
|
||||
onopen*: proc (e: MessageEvent)
|
||||
|
||||
proc newWebSocket*(url, key: cstring): WebSocket
|
||||
{.importcpp: "new WebSocket(@)".}
|
||||
|
||||
proc newWebSocket*(url: cstring): WebSocket
|
||||
{.importcpp: "new WebSocket(@)".}
|
||||
|
||||
proc send*(w: WebSocket; data: cstring) {.importcpp.}
|
||||
132
karax/kajax.nim
Normal file
132
karax/kajax.nim
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
## Karax -- Single page applications for Nim.
|
||||
|
||||
## This module implements support for `ajax`:idx: socket
|
||||
## handling.
|
||||
|
||||
import karax
|
||||
import jsffi except `&`
|
||||
import jscore
|
||||
import dom
|
||||
|
||||
type
|
||||
ProgressEvent* {.importc.}= object
|
||||
loaded*: float
|
||||
total*: float
|
||||
|
||||
FormData* {.importc.} = JsObject
|
||||
|
||||
HttpRequest* {.importc.} = ref object
|
||||
ThisObj {.importc.} = ref object
|
||||
readyState, status: int
|
||||
responseText, statusText: cstring
|
||||
|
||||
XMLHttpRequestUpload* {.importc.} = JsObject
|
||||
|
||||
proc newFormData*(): FormData {.importcpp: "new FormData()".}
|
||||
proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".}
|
||||
proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".}
|
||||
|
||||
proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
|
||||
proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
|
||||
proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
|
||||
proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
|
||||
proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
|
||||
proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
|
||||
|
||||
when not declared(dom.File):
|
||||
type
|
||||
DomFile = ref FileObj
|
||||
FileObj {.importc.} = object of Blob
|
||||
lastModified: int
|
||||
name: cstring
|
||||
else:
|
||||
type
|
||||
DomFile = dom.File
|
||||
|
||||
proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent),
|
||||
cont: proc (httpStatus: int; response: cstring);
|
||||
headers: openarray[(cstring, cstring)] = []) =
|
||||
proc contWrapper(httpStatus: int; response: cstring) =
|
||||
cont(httpStatus, response)
|
||||
|
||||
proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".}
|
||||
var this {.importc: "this".}: ThisObj
|
||||
|
||||
var formData = newFormData()
|
||||
formData.append("upload_file",file)
|
||||
formData.append("filename", DomFile(file).name)
|
||||
let ajax = newRequest()
|
||||
ajax.open("POST", url, true)
|
||||
for a, b in items(headers):
|
||||
ajax.setRequestHeader(a, b)
|
||||
ajax.statechange proc() =
|
||||
if this.readyState == 4:
|
||||
contWrapper(this.status, this.responseText)
|
||||
ajax.upload.onprogress = onprogress
|
||||
ajax.send(formData.to(cstring))
|
||||
|
||||
proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
|
||||
data: cstring;
|
||||
cont: proc (httpStatus: int; response: cstring);
|
||||
doRedraw: bool = true,
|
||||
kxi: KaraxInstance = kxi,
|
||||
useBinary: bool = false,
|
||||
blob: Blob = nil) =
|
||||
proc contWrapper(httpStatus: int; response: cstring) =
|
||||
cont(httpStatus, response)
|
||||
if doRedraw: redraw(kxi)
|
||||
|
||||
var this {.importc: "this".}: ThisObj
|
||||
|
||||
let ajax = newRequest()
|
||||
ajax.open(meth, url, true)
|
||||
for a, b in items(headers):
|
||||
ajax.setRequestHeader(a, b)
|
||||
ajax.statechange proc() =
|
||||
if this.readyState == 4:
|
||||
if this.status == 200:
|
||||
contWrapper(this.status, this.responseText)
|
||||
else:
|
||||
contWrapper(this.status, this.responseText)
|
||||
if useBinary:
|
||||
ajax.send(blob)
|
||||
else:
|
||||
ajax.send(data)
|
||||
|
||||
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
|
||||
data: cstring;
|
||||
cont: proc (httpStatus: int, response: cstring);
|
||||
doRedraw: bool = true,
|
||||
kxi: KaraxInstance = kxi) =
|
||||
ajax("POST", url, headers, data, cont, doRedraw, kxi)
|
||||
|
||||
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
|
||||
data: Blob;
|
||||
cont: proc (httpStatus: int, response: cstring);
|
||||
doRedraw: bool = true,
|
||||
kxi: KaraxInstance = kxi) =
|
||||
ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data)
|
||||
|
||||
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
|
||||
cont: proc (httpStatus: int, response: cstring);
|
||||
doRedraw: bool = true,
|
||||
kxi: KaraxInstance = kxi) =
|
||||
ajax("GET", url, headers, nil, cont, doRedraw, kxi)
|
||||
|
||||
proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)];
|
||||
data: cstring;
|
||||
cont: proc (httpStatus: int, response: cstring);
|
||||
doRedraw: bool = true,
|
||||
kxi: KaraxInstance = kxi) =
|
||||
ajax("PUT", url, headers, data, cont, doRedraw, kxi)
|
||||
|
||||
proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
|
||||
cont: proc (httpStatus: int, response: cstring);
|
||||
doRedraw: bool = true,
|
||||
kxi: KaraxInstance = kxi) =
|
||||
ajax("DELETE", url, headers, nil, cont, doRedraw, kxi)
|
||||
|
||||
|
||||
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
|
||||
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}
|
||||
|
||||
834
karax/karax.nim
Normal file
834
karax/karax.nim
Normal file
|
|
@ -0,0 +1,834 @@
|
|||
## Karax -- Single page applications for Nim.
|
||||
|
||||
import kdom, vdom, jstrutils, compact, jdict, vstyles
|
||||
|
||||
export kdom.Event, kdom.Blob
|
||||
|
||||
when defined(nimNoNil):
|
||||
{.experimental: "notnil".}
|
||||
|
||||
proc kout*[T](x: T) {.importc: "console.log", varargs.}
|
||||
## The preferred way of debugging karax applications.
|
||||
|
||||
type
|
||||
PatchKind = enum
|
||||
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
|
||||
Patch = object
|
||||
k: PatchKind
|
||||
parent, current: Node
|
||||
newNode, oldNode: VNode
|
||||
PatchV = object
|
||||
parent, newChild: VNode
|
||||
pos: int
|
||||
ComponentPair = object
|
||||
oldNode, newNode: VComponent
|
||||
parent, current: Node
|
||||
|
||||
type
|
||||
RouterData* = ref object ## information that is passed to the 'renderer' callback
|
||||
hashPart*: cstring ## the hash part of the URL for routing.
|
||||
|
||||
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
|
||||
## know about this.
|
||||
rootId: cstring
|
||||
renderer: proc (data: RouterData): VNode {.closure.}
|
||||
currentTree: VNode
|
||||
postRenderCallback: proc (data: RouterData)
|
||||
toFocus: Node
|
||||
toFocusV: VNode
|
||||
renderId: int
|
||||
rendering: bool
|
||||
patches: seq[Patch] # we reuse this to save allocations
|
||||
patchLen: int
|
||||
patchesV: seq[PatchV]
|
||||
patchLenV: int
|
||||
runCount: int
|
||||
components: seq[ComponentPair]
|
||||
surpressRedraws*: bool
|
||||
byId: JDict[cstring, VNode]
|
||||
when defined(stats):
|
||||
recursion: int
|
||||
orphans: JDict[cstring, bool]
|
||||
|
||||
|
||||
var
|
||||
kxi*: KaraxInstance ## The current Karax instance. This is always used
|
||||
## as the default. **Note**: Within the karax DSL
|
||||
## always a symbol of the name *kxi* is assumed, so
|
||||
## if you have a local karax instance to use instead
|
||||
## in your 'buildHtml' statement, it needs to be named
|
||||
## 'kxi'.
|
||||
|
||||
proc setFocus*(n: VNode; enabled = true; kxi: KaraxInstance = kxi) =
|
||||
if enabled:
|
||||
kxi.toFocusV = n
|
||||
|
||||
# ----------------- event wrapping ---------------------------------------
|
||||
|
||||
template nativeValue(ev): cstring = cast[Element](ev.target).value
|
||||
template setNativeValue(ev, val) = cast[Element](ev.target).value = val
|
||||
|
||||
template keyeventBody() =
|
||||
let v = nativeValue(ev)
|
||||
n.value = v
|
||||
assert action != nil
|
||||
action(ev, n)
|
||||
if n.value != v:
|
||||
setNativeValue(ev, n.value)
|
||||
# Do not call redraw() here! That is already done
|
||||
# by ``karax.addEventHandler``.
|
||||
|
||||
proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".}
|
||||
proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".}
|
||||
|
||||
proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) =
|
||||
# The DOM is such a pathetic piece of junk that it doesn't
|
||||
# offer 'removeAllEventHandlers()'. Hence we store the event
|
||||
# handler twice in 'd' so that we can emulate this properly.
|
||||
# This is required to fix bug #139.
|
||||
d.addEventListener(name, h)
|
||||
if d.karaxEvents == nil:
|
||||
d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]()
|
||||
d.karaxEvents.add((name, h))
|
||||
|
||||
proc removeAllEventHandlers(d: Node) =
|
||||
if d.karaxEvents != nil:
|
||||
for i in 0..<d.karaxEvents.len:
|
||||
d.removeEventListener(d.karaxEvents[i][0], d.karaxEvents[i][1])
|
||||
|
||||
proc wrapEvent(d: Node; n: VNode; k: EventKind;
|
||||
action: EventHandler): NativeEventHandler =
|
||||
proc stdWrapper(): NativeEventHandler =
|
||||
let action = action
|
||||
let n = n
|
||||
result = proc (ev: Event) =
|
||||
if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input or n.kind == VNodeKind.select:
|
||||
keyeventBody()
|
||||
else: action(ev, n)
|
||||
|
||||
proc enterWrapper(): NativeEventHandler =
|
||||
let action = action
|
||||
let n = n
|
||||
result = proc (ev: Event) =
|
||||
if cast[KeyboardEvent](ev).keyCode == 13: keyeventBody()
|
||||
|
||||
proc laterWrapper(): NativeEventHandler =
|
||||
let action = action
|
||||
let n = n
|
||||
var timer: Timeout
|
||||
result = proc (ev: Event) =
|
||||
proc wrapper() = keyeventBody()
|
||||
if timer != nil: clearTimeout(timer)
|
||||
timer = setTimeout(wrapper, 400)
|
||||
|
||||
case k
|
||||
of EventKind.onkeyuplater:
|
||||
result = laterWrapper()
|
||||
d.addEventShell("keyup", result)
|
||||
of EventKind.onkeyupenter:
|
||||
result = enterWrapper()
|
||||
d.addEventShell("keyup", result)
|
||||
else:
|
||||
result = stdWrapper()
|
||||
d.addEventShell(toEventName[k], result)
|
||||
|
||||
# --------------------- DOM diff -----------------------------------------
|
||||
|
||||
template detach(n: VNode) =
|
||||
addPatch(kxi, pkDetach, nil, nil, nil, n)
|
||||
|
||||
template attach(n: VNode) =
|
||||
n.dom = result
|
||||
if n.id != nil: kxi.byId[n.id] = n
|
||||
|
||||
proc applyEvents(n: VNode) =
|
||||
let dest = n.dom
|
||||
for i in 0..<len(n.events):
|
||||
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
|
||||
|
||||
proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
|
||||
## Get the VNode that was marked with ``id``. Returns ``nil``
|
||||
## if no node exists.
|
||||
if kxi.byId.contains(id):
|
||||
result = kxi.byId[id]
|
||||
|
||||
proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
|
||||
if useAttachedNode:
|
||||
if n.dom != nil:
|
||||
if n.id != nil: kxi.byId[n.id] = n
|
||||
return n.dom
|
||||
if n.kind == VNodeKind.text:
|
||||
result = document.createTextNode(n.text)
|
||||
attach n
|
||||
elif n.kind == VNodeKind.verbatim:
|
||||
result = document.createElement("div")
|
||||
result.innerHTML = n.text
|
||||
attach n
|
||||
return result
|
||||
elif n.kind == VNodeKind.vthunk:
|
||||
let x = callThunk(vcomponents[n.text], n)
|
||||
result = toDom(x, useAttachedNode, kxi)
|
||||
#n.key = result.key
|
||||
attach n
|
||||
return result
|
||||
elif n.kind == VNodeKind.dthunk:
|
||||
result = n.dom #callThunk(dcomponents[n.text], n)
|
||||
assert result != nil
|
||||
#n.key = result.key
|
||||
attach n
|
||||
return result
|
||||
elif n.kind == VNodeKind.component:
|
||||
let x = VComponent(n)
|
||||
if x.onAttachImpl != nil: x.onAttachImpl(x)
|
||||
assert x.renderImpl != nil
|
||||
if x.expanded == nil:
|
||||
x.expanded = x.renderImpl(x)
|
||||
# x.updatedImpl(x, nil)
|
||||
assert x.expanded != nil
|
||||
result = toDom(x.expanded, useAttachedNode, kxi)
|
||||
attach n
|
||||
return result
|
||||
else:
|
||||
result = document.createElement(toTag[n.kind])
|
||||
attach n
|
||||
for k in n:
|
||||
appendChild(result, toDom(k, useAttachedNode, kxi))
|
||||
# text is mapped to 'value':
|
||||
if n.text != nil:
|
||||
result.value = n.text
|
||||
if n.id != nil:
|
||||
result.id = n.id
|
||||
if n.class != nil:
|
||||
result.class = n.class
|
||||
#if n.key >= 0:
|
||||
# result.key = n.key
|
||||
for k, v in attrs(n):
|
||||
if v != nil:
|
||||
result.setAttr(k, v)
|
||||
applyEvents(n)
|
||||
if kxi != nil and n == kxi.toFocusV and kxi.toFocus.isNil:
|
||||
kxi.toFocus = result
|
||||
if not n.style.isNil: applyStyle(result, n.style)
|
||||
|
||||
proc same(n: VNode, e: Node; nesting = 0): bool =
|
||||
if kxi.orphans.contains(n.id): return true
|
||||
if n.kind == VNodeKind.component:
|
||||
result = same(VComponent(n).expanded, e, nesting+1)
|
||||
elif n.kind == VNodeKind.verbatim:
|
||||
result = true
|
||||
elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk:
|
||||
# we don't check these:
|
||||
result = true
|
||||
elif toTag[n.kind] == e.nodename:
|
||||
result = true
|
||||
if n.kind != VNodeKind.text:
|
||||
# BUGFIX: Microsoft's Edge gives the textarea a child containing the text node!
|
||||
if e.len != n.len and n.kind != VNodeKind.textarea:
|
||||
echo "expected ", n.len, " real ", e.len, " ", toTag[n.kind], " nesting ", nesting
|
||||
return false
|
||||
for i in 0 ..< n.len:
|
||||
if not same(n[i], e[i], nesting+1): return false
|
||||
else:
|
||||
echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename
|
||||
|
||||
proc replaceById(id: cstring; newTree: Node) =
|
||||
let x = document.getElementById(id)
|
||||
x.parentNode.replaceChild(newTree, x)
|
||||
newTree.id = id
|
||||
|
||||
type
|
||||
EqResult = enum
|
||||
componentsIdentical, different, similar, identical, usenewNode
|
||||
|
||||
when defined(profileKarax):
|
||||
type
|
||||
DifferEnum = enum
|
||||
deKind, deId, deIndex, deText, deComponent, deClass,
|
||||
deSimilar
|
||||
|
||||
var
|
||||
reasons: array[DifferEnum, int]
|
||||
|
||||
proc echa(a: array[DifferEnum, int]) =
|
||||
for i in low(DifferEnum)..high(DifferEnum):
|
||||
echo i, " value: ", a[i]
|
||||
|
||||
proc eq(a, b: VNode; recursive: bool): EqResult =
|
||||
if a.kind != b.kind:
|
||||
when defined(profileKarax): inc reasons[deKind]
|
||||
return different
|
||||
if a.id != b.id:
|
||||
when defined(profileKarax): inc reasons[deId]
|
||||
return different
|
||||
result = identical
|
||||
if a.index != b.index:
|
||||
when defined(profileKarax): inc reasons[deIndex]
|
||||
return different
|
||||
if a.kind == VNodeKind.text:
|
||||
if a.text != b.text:
|
||||
when defined(profileKarax): inc reasons[deText]
|
||||
return different # similar
|
||||
elif a.kind == VNodeKind.vthunk:
|
||||
if a.text != b.text: return different
|
||||
if a.len != b.len: return different
|
||||
for i in 0..<a.len:
|
||||
if eq(a[i], b[i], recursive) == different: return different
|
||||
elif a.kind == VNodeKind.dthunk:
|
||||
if a.dom == b.dom:
|
||||
return identical
|
||||
else: # fix #119
|
||||
return different
|
||||
elif a.kind == VNodeKind.verbatim:
|
||||
if a.text != b.text:
|
||||
return different
|
||||
elif b.kind == VNodeKind.component:
|
||||
# different component names mean different components:
|
||||
if a.text != b.text:
|
||||
when defined(profileKarax): inc reasons[deComponent]
|
||||
return different
|
||||
#if VComponent(a).key.isNil and VComponent(b).key.isNil:
|
||||
# when defined(profileKarax): inc reasons[deComponent]
|
||||
# return different
|
||||
if VComponent(a).key != VComponent(b).key:
|
||||
when defined(profileKarax): inc reasons[deComponent]
|
||||
return different
|
||||
return componentsIdentical
|
||||
#if:
|
||||
# when defined(profileKarax): inc reasons[deClass]
|
||||
# return different
|
||||
if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b):
|
||||
when defined(profileKarax): inc reasons[deSimilar]
|
||||
return similar
|
||||
|
||||
if recursive:
|
||||
if a.len != b.len:
|
||||
return different
|
||||
for i in 0..<a.len:
|
||||
if eq(a[i], b[i], true) != identical:
|
||||
return different
|
||||
|
||||
# Do not test event listeners here!
|
||||
return result
|
||||
|
||||
proc updateStyles(newNode, oldNode: VNode) =
|
||||
# we keep the oldNode, but take over the style from the new node:
|
||||
if oldNode.dom != nil:
|
||||
if newNode.style != nil: applyStyle(oldNode.dom, newNode.style)
|
||||
else: oldNode.dom.style = Style()
|
||||
oldNode.dom.class = newNode.class
|
||||
oldNode.style = newNode.style
|
||||
oldNode.class = newNode.class
|
||||
|
||||
proc updateAttributes(newNode, oldNode: VNode) =
|
||||
# we keep the oldNode, but take over the attributes from the new node:
|
||||
if oldNode.dom != nil:
|
||||
for k, _ in attrs(oldNode):
|
||||
oldNode.dom.removeAttribute(k)
|
||||
for k, v in attrs(newNode):
|
||||
if v != nil:
|
||||
oldNode.dom.setAttr(k, v)
|
||||
takeOverAttr(newNode, oldNode)
|
||||
|
||||
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
|
||||
let d = oldNode.dom
|
||||
if d != nil:
|
||||
removeAllEventHandlers(d)
|
||||
when false:
|
||||
for i in 0..<oldNode.events.len:
|
||||
let k = oldNode.events[i][0]
|
||||
let name = case k
|
||||
of EventKind.onkeyuplater, EventKind.onkeyupenter: cstring"keyup"
|
||||
else: toEventName[k]
|
||||
d.removeEventListener(name, oldNode.events[i][2])
|
||||
shallowCopy(oldNode.events, newNode.events)
|
||||
applyEvents(oldNode)
|
||||
|
||||
when false:
|
||||
proc printV(n: VNode; depth: cstring = "") =
|
||||
kout depth, cstring($n.kind), cstring"key ", n.index
|
||||
#for k, v in pairs(n.style):
|
||||
# kout depth, "style: ", k, v
|
||||
if n.kind == VNodeKind.component:
|
||||
let nn = VComponent(n)
|
||||
if nn.expanded != nil: printV(nn.expanded, ">>" & depth)
|
||||
elif n.kind == VNodeKind.text:
|
||||
kout depth, n.text
|
||||
for i in 0 ..< n.len:
|
||||
printV(n[i], depth & " ")
|
||||
|
||||
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
|
||||
na, oldNode: VNode) =
|
||||
let L = kxi.patchLen
|
||||
if L >= kxi.patches.len:
|
||||
# allocate more space:
|
||||
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
|
||||
newNode: na, oldNode: oldNode))
|
||||
else:
|
||||
kxi.patches[L].k = ka
|
||||
kxi.patches[L].parent = parenta
|
||||
kxi.patches[L].current = currenta
|
||||
kxi.patches[L].newNode = na
|
||||
kxi.patches[L].oldNode = oldNode
|
||||
inc kxi.patchLen
|
||||
|
||||
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
|
||||
let L = kxi.patchLenV
|
||||
if L >= kxi.patchesV.len:
|
||||
# allocate more space:
|
||||
kxi.patchesV.add(PatchV(parent: parent, newChild: newChild, pos: pos))
|
||||
else:
|
||||
kxi.patchesV[L].parent = parent
|
||||
kxi.patchesV[L].newChild = newChild
|
||||
kxi.patchesV[L].pos = pos
|
||||
inc kxi.patchLenV
|
||||
|
||||
proc moveDom(dest, src: VNode) =
|
||||
dest.dom = src.dom
|
||||
src.dom = nil
|
||||
if dest.id != nil:
|
||||
kxi.byId[dest.id] = dest
|
||||
assert dest.len == src.len
|
||||
for i in 0..<dest.len:
|
||||
moveDom(dest[i], src[i])
|
||||
|
||||
proc applyPatch(kxi: KaraxInstance) =
|
||||
for i in 0..<kxi.patchLen:
|
||||
let p = kxi.patches[i]
|
||||
case p.k
|
||||
of pkReplace:
|
||||
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||
if p.parent == nil:
|
||||
replaceById(kxi.rootId, nn)
|
||||
else:
|
||||
if p.current.parentNode == p.parent:
|
||||
p.parent.replaceChild(nn, p.current)
|
||||
else: # fix #121
|
||||
p.parent.appendChild(nn)
|
||||
of pkSame:
|
||||
moveDom(p.newNode, p.oldNode)
|
||||
of pkRemove:
|
||||
p.parent.removeChild(p.current)
|
||||
of pkAppend:
|
||||
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||
p.parent.appendChild(nn)
|
||||
of pkInsertBefore:
|
||||
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||
p.parent.insertBefore(nn, p.current)
|
||||
of pkDetach:
|
||||
let n = p.oldNode
|
||||
if n.id != nil: kxi.byId.del(n.id)
|
||||
if n.kind == VNodeKind.component:
|
||||
let x = VComponent(n)
|
||||
if x.onDetachImpl != nil: x.onDetachImpl(x)
|
||||
# XXX for some reason this causes assertion errors otherwise:
|
||||
if not kxi.surpressRedraws: n.dom = nil
|
||||
kxi.patchLen = 0
|
||||
for i in 0..<kxi.patchLenV:
|
||||
let p = kxi.patchesV[i]
|
||||
p.parent[p.pos] = p.newChild
|
||||
assert p.newChild.dom != nil
|
||||
kxi.patchLenV = 0
|
||||
|
||||
# ASSUME: We patch both the virtual DOM and the real DOM and throw away
|
||||
# the newly produced DOM. Thus on updates like 'newNode.dom = oldNode.dom'
|
||||
# are required. The only exception is when the top level node is replaced.
|
||||
# Then we have to take the new virtual DOM. In fact, we trigger a full DOM
|
||||
# rebuild then. However, we don't have to consider old event handlers then
|
||||
# so everything stays simple.
|
||||
# We also do not produce "Patch sets" anymore, everything is done as simply
|
||||
# as possible. Ok, let's assume that we seek to update event handler lists:
|
||||
# The new node has captures to itself or to other new nodes, never to old
|
||||
# nodes! --> We cannot ever use the old VDOM, we have to use the new virtual
|
||||
# DOM. For identical nodes we need to take over the .dom field from the old
|
||||
# node since we don't recompute them. This must be done recursively. In
|
||||
# vnodeToDom we have to check whether the 'dom' field was already set. If so,
|
||||
# There is nothing to do.
|
||||
# "Similar" nodes can have the opposite effect; consider
|
||||
#
|
||||
# AAAABAAAA
|
||||
# AAAACDAAAA
|
||||
#
|
||||
# In this example B did change to C and 'D' is new. However, replacing B by
|
||||
# CD is fine.
|
||||
#
|
||||
|
||||
|
||||
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
|
||||
when defined(stats):
|
||||
if kxi.recursion > 100:
|
||||
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
|
||||
if oldNode.kind == VNodeKind.text:
|
||||
echo oldNode.text
|
||||
inc kxi.recursion
|
||||
let result = eq(newNode, oldNode, false)
|
||||
case result
|
||||
of componentsIdentical:
|
||||
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
|
||||
newNode: VComponent(newNode),
|
||||
parent: parent,
|
||||
current: current)
|
||||
of identical, similar:
|
||||
newNode.dom = oldNode.dom
|
||||
if result == similar:
|
||||
updateStyles(newNode, oldNode)
|
||||
updateAttributes(newNode, oldNode)
|
||||
if oldNode.kind == VNodeKind.text:
|
||||
oldNode.text = newNode.text
|
||||
oldNode.dom.nodeValue = newNode.text
|
||||
|
||||
if newNode.events.len != 0 or oldNode.events.len != 0:
|
||||
mergeEvents(newNode, oldNode, kxi)
|
||||
|
||||
let newLength = newNode.len
|
||||
let oldLength = oldNode.len
|
||||
if newLength == 0 and oldLength == 0: return
|
||||
let minLength = min(newLength, oldLength)
|
||||
|
||||
assert oldNode.kind == newNode.kind
|
||||
var commonPrefix = 0
|
||||
|
||||
# compute common prefix:
|
||||
while commonPrefix < minLength:
|
||||
if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
|
||||
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
|
||||
inc commonPrefix
|
||||
else:
|
||||
break
|
||||
|
||||
# compute common suffix:
|
||||
var oldPos = oldLength - 1
|
||||
var newPos = newLength - 1
|
||||
while oldPos >= commonPrefix and newPos >= commonPrefix:
|
||||
if eq(newNode[newPos], oldNode[oldPos], true) == identical:
|
||||
kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
|
||||
dec oldPos
|
||||
dec newPos
|
||||
else:
|
||||
break
|
||||
|
||||
let pos = min(oldPos, newPos) + 1
|
||||
# now the different children are in commonPrefix .. pos - 1:
|
||||
for i in commonPrefix..pos-1:
|
||||
diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
|
||||
|
||||
if oldPos + 1 == oldLength:
|
||||
for i in pos..newPos:
|
||||
kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
|
||||
else:
|
||||
let before = current.childNodes[oldPos + 1]
|
||||
for i in pos..newPos:
|
||||
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
|
||||
# XXX call 'attach' here?
|
||||
for i in pos..oldPos:
|
||||
detach(oldNode[i])
|
||||
#doAssert i < current.childNodes.len
|
||||
kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
|
||||
of different:
|
||||
detach(oldNode)
|
||||
kxi.addPatch(pkReplace, parent, current, newNode, nil)
|
||||
of usenewNode: doAssert(false, "eq returned usenewNode")
|
||||
when defined(stats):
|
||||
dec kxi.recursion
|
||||
|
||||
proc applyComponents(kxi: KaraxInstance) =
|
||||
# the first 'diff' pass detects components in the VDOM. The
|
||||
# 'applyComponents' expands components and so on until no
|
||||
# components are left to check.
|
||||
var i = 0
|
||||
# beware: 'diff' appends to kxi.components!
|
||||
# So this is actually a fixpoint iteration:
|
||||
while i < kxi.components.len:
|
||||
let x = kxi.components[i].oldNode
|
||||
let newNode = kxi.components[i].newNode
|
||||
when defined(karaxDebug):
|
||||
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
|
||||
if x.changedImpl != nil and x.changedImpl(x, newNode):
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, " did change"
|
||||
let current = kxi.components[i].current
|
||||
let parent = kxi.components[i].parent
|
||||
x.updatedImpl(x, newNode)
|
||||
let oldExpanded = x.expanded
|
||||
x.expanded = x.renderImpl(x)
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, " re-rendered"
|
||||
x.renderedVersion = x.version
|
||||
if oldExpanded == nil:
|
||||
detach(x)
|
||||
kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, ": old expansion didn't exist"
|
||||
else:
|
||||
diff(x.expanded, oldExpanded, parent, current, kxi)
|
||||
when false:
|
||||
if res == usenewNode:
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
|
||||
discard "diff created a patchset for us, so this is fine"
|
||||
elif res != different:
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
|
||||
x.expanded = oldExpanded
|
||||
assert oldExpanded.dom != nil, "old expanded.dom is nil"
|
||||
else:
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
|
||||
assert x.expanded.dom != nil, "expanded.dom is nil"
|
||||
inc i
|
||||
setLen(kxi.components, 0)
|
||||
|
||||
when defined(stats):
|
||||
proc depth(n: VNode; total: var int): int =
|
||||
var m = 0
|
||||
for i in 0..<n.len:
|
||||
m = max(m, depth(n[i], total))
|
||||
result = m + 1
|
||||
inc total
|
||||
|
||||
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
|
||||
detach(parent[position])
|
||||
let current = parent.dom
|
||||
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil)
|
||||
parent.delete(position)
|
||||
applyPatch(kxi)
|
||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||
|
||||
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
|
||||
let current = parent.dom
|
||||
if position >= parent.len:
|
||||
kxi.addPatch(pkAppend, current, nil, kid, nil)
|
||||
parent.add(kid)
|
||||
else:
|
||||
let before = current.childNodes[position]
|
||||
kxi.addPatch(pkInsertBefore, current, before, kid, nil)
|
||||
parent.insert(kid, position)
|
||||
applyPatch(kxi)
|
||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||
|
||||
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
|
||||
let olddom = oldNode.dom
|
||||
doAssert olddom != nil
|
||||
diff(newNode, oldNode, nil, olddom, kxi)
|
||||
# this is a bit nasty: Since we cannot patch the 'parent' of
|
||||
# the current VNode (because we don't store it at all!), we
|
||||
# need to override the fields individually:
|
||||
takeOverFields(newNode, oldNode)
|
||||
applyComponents(kxi)
|
||||
applyPatch(kxi)
|
||||
if kxi.currentTree == oldNode:
|
||||
kxi.currentTree = newNode
|
||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||
|
||||
var onhashChange {.importc: "window.onhashchange".}: proc()
|
||||
var hashPart {.importc: "window.location.hash".}: cstring
|
||||
|
||||
proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
|
||||
## enforce a full redraw for the next redraw operation.
|
||||
## This can be used as a temporary way to workaround DOM diffing
|
||||
## problems or to avoid the DOM diffing when you already know
|
||||
## it should use a completely new DOM.
|
||||
## This is an experimental API.
|
||||
kxi.currentTree = nil
|
||||
|
||||
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
|
||||
when false:
|
||||
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
|
||||
|
||||
proc dodraw(kxi: KaraxInstance) =
|
||||
if kxi.renderer.isNil: return
|
||||
kxi.renderId = 0
|
||||
|
||||
if kxi.rendering:
|
||||
# there is a render already running, delay 1 frame
|
||||
kxi.renderId = reqFrame(proc () = kxi.dodraw)
|
||||
return
|
||||
|
||||
kxi.rendering = true
|
||||
|
||||
let rdata = RouterData(hashPart: hashPart)
|
||||
let newtree = kxi.renderer(rdata)
|
||||
inc kxi.runCount
|
||||
newtree.id = kxi.rootId
|
||||
kxi.toFocus = nil
|
||||
if kxi.currentTree == nil:
|
||||
let asdom = toDom(newtree, useAttachedNode = true, kxi)
|
||||
replaceById(kxi.rootId, asdom)
|
||||
else:
|
||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||
let olddom = document.getElementById(kxi.rootId)
|
||||
diff(newtree, kxi.currentTree, nil, olddom, kxi)
|
||||
when defined(profileKarax):
|
||||
echo "<<<<<<<<<<<<<<"
|
||||
echa reasons
|
||||
applyComponents(kxi)
|
||||
when defined(profileKarax):
|
||||
echo "--------------"
|
||||
echa reasons
|
||||
echo ">>>>>>>>>>>>>>"
|
||||
applyPatch(kxi)
|
||||
kxi.currentTree = newtree
|
||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||
|
||||
if not kxi.postRenderCallback.isNil:
|
||||
kxi.postRenderCallback(rdata)
|
||||
|
||||
# now that it's part of the DOM, give it the focus:
|
||||
if kxi.toFocus != nil:
|
||||
kxi.toFocus.focus()
|
||||
kxi.rendering = false
|
||||
when defined(stats):
|
||||
kxi.recursion = 0
|
||||
var total = 0
|
||||
echo "depth ", depth(kxi.currentTree, total), " total ", total
|
||||
|
||||
proc redraw*(kxi: KaraxInstance = kxi) =
|
||||
# we buffer redraw requests:
|
||||
when false:
|
||||
if drawTimeout != nil:
|
||||
clearTimeout(drawTimeout)
|
||||
drawTimeout = setTimeout(dodraw, 30)
|
||||
elif true:
|
||||
if kxi.renderId == 0:
|
||||
kxi.renderId = reqFrame(proc () = kxi.dodraw)
|
||||
else:
|
||||
dodraw(kxi)
|
||||
|
||||
proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi)
|
||||
|
||||
proc init(ev: Event) =
|
||||
kxi.renderId = reqFrame(proc () = kxi.dodraw)
|
||||
|
||||
proc setRenderer*(renderer: proc (data: RouterData): VNode,
|
||||
root: cstring = "ROOT",
|
||||
clientPostRenderCallback:
|
||||
proc (data: RouterData) = nil): KaraxInstance {.
|
||||
discardable.} =
|
||||
## Setup Karax. Usually the return value can be ignored.
|
||||
if document.getElementById(root).isNil:
|
||||
let msg = "Could not find a <div> with id=" & root &
|
||||
". Karax needs it as its rendering target."
|
||||
raise newException(Exception, $msg)
|
||||
|
||||
result = KaraxInstance(rootId: root, renderer: renderer,
|
||||
postRenderCallback: clientPostRenderCallback,
|
||||
patches: newSeq[Patch](60),
|
||||
patchesV: newSeq[PatchV](30),
|
||||
components: @[],
|
||||
surpressRedraws: false,
|
||||
byId: newJDict[cstring, VNode](),
|
||||
orphans: newJDict[cstring, bool]())
|
||||
kxi = result
|
||||
window.onload = init
|
||||
onhashChange = proc() = redraw()
|
||||
|
||||
proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
|
||||
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
|
||||
## Setup Karax. Usually the return value can be ignored.
|
||||
proc wrapRenderer(data: RouterData): VNode = result = renderer()
|
||||
proc wrapPostRender(data: RouterData) =
|
||||
if clientPostRenderCallback != nil: clientPostRenderCallback()
|
||||
setRenderer(wrapRenderer, root, wrapPostRender)
|
||||
|
||||
proc setInitializer*(renderer: proc (data: RouterData): VNode, root: cstring = "ROOT",
|
||||
clientPostRenderCallback:
|
||||
proc (data: RouterData) = nil): KaraxInstance {.discardable.} =
|
||||
## Setup Karax. Usually the return value can be ignored.
|
||||
result = KaraxInstance(rootId: root, renderer: renderer,
|
||||
postRenderCallback: clientPostRenderCallback,
|
||||
patches: newSeq[Patch](60),
|
||||
patchesV: newSeq[PatchV](30),
|
||||
components: @[],
|
||||
surpressRedraws: true,
|
||||
byId: newJDict[cstring, VNode](),
|
||||
orphans: newJDict[cstring, bool]())
|
||||
kxi = result
|
||||
window.onload = init
|
||||
|
||||
proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
|
||||
kxi: KaraxInstance = kxi) =
|
||||
## Implements the foundation of Karax's event management.
|
||||
## Karax DSL transforms ``tag(onEvent = handler)`` to
|
||||
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
|
||||
## where ``wrapper`` calls the passed ``action`` and then triggers
|
||||
## a ``redraw``.
|
||||
proc wrapper(ev: Event; n: VNode) =
|
||||
action(ev, n)
|
||||
if not kxi.surpressRedraws: redraw(kxi)
|
||||
addEventListener(n, k, wrapper)
|
||||
|
||||
proc addEventHandler*(n: VNode; k: EventKind; action: proc();
|
||||
kxi: KaraxInstance = kxi) =
|
||||
## Implements the foundation of Karax's event management.
|
||||
## Karax DSL transforms ``tag(onEvent = handler)`` to
|
||||
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
|
||||
## where ``wrapper`` calls the passed ``action`` and then triggers
|
||||
## a ``redraw``.
|
||||
proc wrapper(ev: Event; n: VNode) =
|
||||
action()
|
||||
if not kxi.surpressRedraws: redraw(kxi)
|
||||
addEventListener(n, k, wrapper)
|
||||
|
||||
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
|
||||
## Now deprecated, instead pass a callback to ``setRenderer`` that receives
|
||||
## a ``data: RouterData`` parameter.
|
||||
proc wrapper() =
|
||||
action(hashPart)
|
||||
redraw()
|
||||
onhashchange = wrapper
|
||||
|
||||
proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) =
|
||||
## Declares a node ID as "foreign". Foreign nodes are not
|
||||
## under Karax's control in the sense that Karax does not attempt
|
||||
## to perform structural checks on them.
|
||||
kxi.orphans[id] = true
|
||||
|
||||
{.push stackTrace:off.}
|
||||
proc setupErrorHandler*() =
|
||||
## Installs an error handler that transforms native JS unhandled
|
||||
## exceptions into Nim based stack traces. If `useAlert` is false,
|
||||
## the error message is put into the console, otherwise `alert`
|
||||
## is called.
|
||||
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
|
||||
var onerror {.importc: "window.onerror", used.} =
|
||||
proc (msg, url: cstring, line, col: int, error: cstring): bool =
|
||||
var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
|
||||
echo(x)
|
||||
return true # suppressErrorAlert
|
||||
{.pop.}
|
||||
|
||||
proc prepend(parent, kid: Element) =
|
||||
parent.insertBefore(kid, parent.firstChild)
|
||||
|
||||
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
|
||||
let s = document.createElement("script")
|
||||
s.setAttr "type", "text/javascript"
|
||||
s.setAttr "src", jsfilename
|
||||
document.body.prepend(s)
|
||||
redraw(kxi)
|
||||
|
||||
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
|
||||
proc wrapper() =
|
||||
action()
|
||||
redraw()
|
||||
result = setTimeout(wrapper, later)
|
||||
|
||||
proc setInputText*(n: VNode; s: cstring) =
|
||||
## Sets the text of input elements.
|
||||
n.text = s
|
||||
if n.dom != nil: n.dom.value = s
|
||||
|
||||
proc getInputText*(n: VNode): cstring =
|
||||
if n.dom != nil:
|
||||
result = n.dom.value
|
||||
|
||||
proc toChecked*(checked: bool): cstring =
|
||||
(if checked: cstring"checked" else: cstring(nil))
|
||||
|
||||
proc toDisabled*(disabled: bool): cstring =
|
||||
(if disabled: cstring"disabled" else: cstring(nil))
|
||||
|
||||
proc toSelected*(selected: bool): cstring =
|
||||
(if selected: cstring"selected" else: cstring(nil))
|
||||
|
||||
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
|
||||
result = toDom(n, useAttachedNode = false, kxi)
|
||||
200
karax/karaxdsl.nim
Normal file
200
karax/karaxdsl.nim
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
|
||||
import macros, vdom, compact, kbase
|
||||
from strutils import startsWith, toLowerAscii
|
||||
|
||||
when defined(js):
|
||||
import karax
|
||||
|
||||
const
|
||||
StmtContext = ["kout", "inc", "echo", "dec", "!"]
|
||||
SpecialAttrs = ["id", "class", "value", "index", "style"]
|
||||
|
||||
proc getName(n: NimNode): string =
|
||||
case n.kind
|
||||
of nnkIdent, nnkSym:
|
||||
result = $n
|
||||
of nnkAccQuoted:
|
||||
result = ""
|
||||
for i in 0..<n.len:
|
||||
result.add getName(n[i])
|
||||
of nnkStrLit..nnkTripleStrLit:
|
||||
result = n.strVal
|
||||
of nnkInfix:
|
||||
# allow 'foo-bar' syntax:
|
||||
if n.len == 3 and $n[0] == "-":
|
||||
result = getName(n[1]) & "-" & getName(n[2])
|
||||
else:
|
||||
expectKind(n, nnkIdent)
|
||||
of nnkDotExpr:
|
||||
result = getName(n[0]) & "." & getName(n[1])
|
||||
of nnkOpenSymChoice, nnkClosedSymChoice:
|
||||
result = getName(n[0])
|
||||
else:
|
||||
#echo repr n
|
||||
expectKind(n, nnkIdent)
|
||||
|
||||
proc toKstring(n: NimNode): NimNode =
|
||||
if n.kind == nnkStrLit:
|
||||
result = newCall(bindSym"kstring", n)
|
||||
else:
|
||||
result = copyNimNode(n)
|
||||
for child in n:
|
||||
result.add toKstring(child)
|
||||
|
||||
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
|
||||
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
|
||||
|
||||
proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||
# we need to distinguish statement and expression contexts:
|
||||
# every call statement 's' needs to be transformed to 'dest.add s'.
|
||||
# If expressions need to be distinguished from if statements. Since
|
||||
# we know we start in a statement context, it's pretty simple to
|
||||
# figure out expression contexts: In calls everything is an expression
|
||||
# (except for the last child of the macros we consider here),
|
||||
# lets, consts, types can be considered as expressions
|
||||
# case is complex, calls are assumed to produce a value.
|
||||
when defined(js):
|
||||
template evHandler(): untyped = bindSym"addEventHandler"
|
||||
else:
|
||||
template evHandler(): untyped = ident"addEventHandler"
|
||||
|
||||
case n.kind
|
||||
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
|
||||
if tmpContext != nil:
|
||||
result = newCall(bindSym"add", tmpContext, n)
|
||||
else:
|
||||
result = n
|
||||
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
|
||||
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
|
||||
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
|
||||
# recurse for the last son:
|
||||
result = copyNimTree(n)
|
||||
let L = n.len
|
||||
assert n.len == result.len
|
||||
if L > 0:
|
||||
result[L-1] = tcall2(result[L-1], tmpContext)
|
||||
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
|
||||
nnkFinally:
|
||||
# recurse for every child:
|
||||
result = copyNimNode(n)
|
||||
for x in n:
|
||||
result.add tcall2(x, tmpContext)
|
||||
of nnkCaseStmt:
|
||||
# recurse for children, but don't add call for case ident
|
||||
result = copyNimNode(n)
|
||||
result.add n[0]
|
||||
for i in 1 ..< n.len:
|
||||
result.add tcall2(n[i], tmpContext)
|
||||
of nnkProcDef:
|
||||
let name = getName n[0]
|
||||
if name.startsWith"on":
|
||||
# turn it into an anon proc:
|
||||
let anon = copyNimTree(n)
|
||||
anon[0] = newEmptyNode()
|
||||
if tmpContext == nil:
|
||||
error "no VNode to attach the event handler to"
|
||||
else:
|
||||
result = newCall(evHandler(), tmpContext,
|
||||
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
|
||||
else:
|
||||
result = n
|
||||
of nnkVarSection, nnkLetSection, nnkConstSection:
|
||||
result = n
|
||||
of nnkCallKinds - {nnkInfix}:
|
||||
let op = getName(n[0])
|
||||
let ck = isComponent(op)
|
||||
if ck != ComponentKind.None:
|
||||
let tmp = genSym(nskLet, "tmp")
|
||||
let call = if ck == ComponentKind.Tag:
|
||||
newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))
|
||||
elif ck == ComponentKind.VNode:
|
||||
newCall(bindSym"vthunk", newLit(op))
|
||||
else:
|
||||
newCall(bindSym"dthunk", newLit(op))
|
||||
result = newTree(
|
||||
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
|
||||
newLetStmt(tmp, call))
|
||||
for i in 1 ..< n.len:
|
||||
# named parameters are transformed into attributes or events:
|
||||
let x = n[i]
|
||||
if x.kind == nnkExprEqExpr:
|
||||
let key = getName x[0]
|
||||
if key.startsWith("on"):
|
||||
result.add newCall(evHandler(),
|
||||
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
|
||||
elif eqIdent(key, "style") and x[1].kind == nnkTableConstr:
|
||||
result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1]))
|
||||
elif key in SpecialAttrs:
|
||||
result.add newDotAsgn(tmp, key, x[1])
|
||||
if key == "value":
|
||||
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
|
||||
elif eqIdent(key, "setFocus"):
|
||||
result.add newCall(key, tmp, x[1], ident"kxi")
|
||||
elif eqIdent(key, "events"):
|
||||
result.add newCall(bindSym"mergeEvents", tmp, x[1])
|
||||
else:
|
||||
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
|
||||
elif ck != ComponentKind.Tag:
|
||||
call.add x
|
||||
elif eqIdent(x, "setFocus"):
|
||||
result.add newCall(x, tmp, bindSym"true", ident"kxi")
|
||||
else:
|
||||
result.add tcall2(x, tmp)
|
||||
if tmpContext == nil:
|
||||
result.add tmp
|
||||
else:
|
||||
result.add newCall(bindSym"add", tmpContext, tmp)
|
||||
elif tmpContext != nil and op notin StmtContext:
|
||||
var hasEventHandlers = false
|
||||
for i in 1..<n.len:
|
||||
let it = n[i]
|
||||
if it.kind in {nnkProcDef, nnkStmtList}:
|
||||
hasEventHandlers = true
|
||||
break
|
||||
if not hasEventHandlers:
|
||||
result = newCall(bindSym"add", tmpContext, n)
|
||||
else:
|
||||
let tmp = genSym(nskLet, "tmp")
|
||||
var slicedCall = newCall(n[0])
|
||||
let ex = newTree(nnkStmtListExpr)
|
||||
ex.add newEmptyNode() # will become the let statement
|
||||
for i in 1..<n.len:
|
||||
let it = n[i]
|
||||
if it.kind in {nnkProcDef, nnkStmtList}:
|
||||
ex.add tcall2(it, tmp)
|
||||
else:
|
||||
slicedCall.add it
|
||||
ex[0] = newLetStmt(tmp, slicedCall)
|
||||
ex.add tmp
|
||||
result = newCall(bindSym"add", tmpContext, ex)
|
||||
elif op == "!" and n.len == 2:
|
||||
result = n[1]
|
||||
else:
|
||||
result = n
|
||||
else:
|
||||
result = n
|
||||
|
||||
macro buildHtml*(tag, children: untyped): VNode =
|
||||
let kids = newProc(procType=nnkDo, body=children)
|
||||
expectKind kids, nnkDo
|
||||
var call: NimNode
|
||||
if tag.kind in nnkCallKinds:
|
||||
call = tag
|
||||
else:
|
||||
call = newCall(tag)
|
||||
call.add body(kids)
|
||||
result = tcall2(call, nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
|
||||
macro buildHtml*(children: untyped): VNode =
|
||||
let kids = newProc(procType=nnkDo, body=children)
|
||||
expectKind kids, nnkDo
|
||||
result = tcall2(body(kids), nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
|
||||
macro flatHtml*(tag: untyped): VNode =
|
||||
result = tcall2(tag, nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
8
karax/kbase.nim
Normal file
8
karax/kbase.nim
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
## Karax -- Single page applications for Nim.
|
||||
|
||||
## Basic type definitions for Karax.
|
||||
when defined(js):
|
||||
type kstring* = cstring
|
||||
else:
|
||||
type kstring* = string
|
||||
|
||||
10
karax/kdom.nim
Normal file
10
karax/kdom.nim
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
## Karax -- Single page applications for Nim.
|
||||
|
||||
from dom import nil
|
||||
|
||||
when not declared(dom.DomApiVersion):
|
||||
include kdom_impl
|
||||
|
||||
else:
|
||||
import dom
|
||||
export dom
|
||||
1232
karax/kdom_impl.nim
Normal file
1232
karax/kdom_impl.nim
Normal file
File diff suppressed because it is too large
Load diff
616
karax/languages.nim
Normal file
616
karax/languages.nim
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
## Karax -- Single page applications for Nim.
|
||||
|
||||
type
|
||||
Language* {.pure.} = enum ## A pure enum that contains most (all?)
|
||||
## known human languages in current use.
|
||||
afZA ## "South Africa - Afrikaans"
|
||||
amET ## "Ethiopia - Amharic"
|
||||
arAE ## "U.A.E. - Arabic"
|
||||
arBH ## "Bahrain - Arabic"
|
||||
arDZ ## "Algeria - Arabic"
|
||||
arEG ## "Egypt - Arabic"
|
||||
arIQ ## "Iraq - Arabic"
|
||||
arJO ## "Jordan - Arabic"
|
||||
arKW ## "Kuwait - Arabic"
|
||||
arLB ## "Lebanon - Arabic"
|
||||
arLY ## "Libya - Arabic"
|
||||
arMA ## "Morocco - Arabic"
|
||||
arOM ## "Oman - Arabic"
|
||||
arQA ## "Qatar - Arabic"
|
||||
arSA ## "Saudi Arabia - Arabic"
|
||||
arSY ## "Syria - Arabic"
|
||||
arTN ## "Tunisia - Arabic"
|
||||
arYE ## "Yemen - Arabic"
|
||||
arnCL ## "Chile - Mapudungun"
|
||||
asIN ## "India - Assamese"
|
||||
azAZ ## "Azerbaijan - Azeri (Cyrillic)"
|
||||
baRU ## "Russia - Bashkir"
|
||||
beBY ## "Belarus - Belarusian"
|
||||
bgBG ## "Bulgaria - Bulgarian"
|
||||
bnBD ## "Bangladesh - Bengali"
|
||||
bnIN ## "India - Bengali"
|
||||
boCN ## "People's Republic of China - Tibetan"
|
||||
brFR ## "France - Breton"
|
||||
bsBA ## "Bosnia and Herzegovina - Bosnian (Cyrillic)"
|
||||
caES ## "Spain - Catalan"
|
||||
coFR ## "France - Corsican"
|
||||
csCZ ## "Czech Republic - Czech"
|
||||
cyGB ## "United Kingdom - Welsh"
|
||||
daDK ## "Denmark - Danish"
|
||||
deAT ## "Austria - German"
|
||||
deCH ## "Switzerland - German"
|
||||
deDE ## "Germany - German"
|
||||
deLI ## "Liechtenstein - German"
|
||||
deLU ## "Luxembourg - German"
|
||||
dsbDE ## "Germany - Lower Sorbian"
|
||||
dvMV ## "Maldives - Divehi"
|
||||
elGR ## "Greece - Greek"
|
||||
en029 ## "Caribbean - English"
|
||||
enAU ## "Australia - English"
|
||||
enBZ ## "Belize - English"
|
||||
enCA ## "Canada - English"
|
||||
enGB ## "United Kingdom - English"
|
||||
enIE ## "Ireland - English"
|
||||
enIN ## "India - English"
|
||||
enJM ## "Jamaica - English"
|
||||
enMY ## "Malaysia - English"
|
||||
enNZ ## "New Zealand - English"
|
||||
enPH ## "Republic of the Philippines - English"
|
||||
enSG ## "Singapore - English"
|
||||
enTT ## "Trinidad and Tobago - English"
|
||||
enUS ## "United States - English"
|
||||
enZA ## "South Africa - English"
|
||||
enZW ## "Zimbabwe - English"
|
||||
esAR ## "Argentina - Spanish"
|
||||
esBO ## "Bolivia - Spanish"
|
||||
esCL ## "Chile - Spanish"
|
||||
esCO ## "Colombia - Spanish"
|
||||
esCR ## "Costa Rica - Spanish"
|
||||
esDO ## "Dominican Republic - Spanish"
|
||||
esEC ## "Ecuador - Spanish"
|
||||
esES ## "Spain - Spanish"
|
||||
esGT ## "Guatemala - Spanish"
|
||||
esHN ## "Honduras - Spanish"
|
||||
esMX ## "Mexico - Spanish"
|
||||
esNI ## "Nicaragua - Spanish"
|
||||
esPA ## "Panama - Spanish"
|
||||
esPE ## "Peru - Spanish"
|
||||
esPR ## "Puerto Rico - Spanish"
|
||||
esPY ## "Paraguay - Spanish"
|
||||
esSV ## "El Salvador - Spanish"
|
||||
esUS ## "United States - Spanish"
|
||||
esUY ## "Uruguay - Spanish"
|
||||
esVE ## "Bolivarian Republic of Venezuela - Spanish"
|
||||
etEE ## "Estonia - Estonian"
|
||||
euES ## "Spain - Basque"
|
||||
faIR ## "Iran - Persian"
|
||||
fiFI ## "Finland - Finnish"
|
||||
filPH ## "Philippines - Filipino"
|
||||
foFO ## "Faroe Islands - Faroese"
|
||||
frBE ## "Belgium - French"
|
||||
frCA ## "Canada - French"
|
||||
frCH ## "Switzerland - French"
|
||||
frFR ## "France - French"
|
||||
frLU ## "Luxembourg - French"
|
||||
frMC ## "Principality of Monaco - French"
|
||||
fyNL ## "Netherlands - Frisian"
|
||||
gaIE ## "Ireland - Irish"
|
||||
gdGB ## "United Kingdom - Scottish Gaelic"
|
||||
glES ## "Spain - Galician"
|
||||
gswFR ## "France - Alsatian"
|
||||
guIN ## "India - Gujarati"
|
||||
haNG ## "Nigeria - Hausa (Latin)"
|
||||
heIL ## "Israel - Hebrew"
|
||||
hiIN ## "India - Hindi"
|
||||
hrBA ## "Bosnia and Herzegovina - Croatian"
|
||||
hrHR ## "Croatia - Croatian"
|
||||
hsbDE ## "Germany - Upper Sorbian"
|
||||
huHU ## "Hungary - Hungarian"
|
||||
hyAM ## "Armenia - Armenian"
|
||||
idID ## "Indonesia - Indonesian"
|
||||
igNG ## "Nigeria - Igbo"
|
||||
iiCN ## "People's Republic of China - Yi"
|
||||
isIS ## "Iceland - Icelandic"
|
||||
itCH ## "Switzerland - Italian"
|
||||
itIT ## "Italy - Italian"
|
||||
iuCA ## "Canada - Inuktitut (Latin)"
|
||||
jaJP ## "Japan - Japanese"
|
||||
kaGE ## "Georgia - Georgian"
|
||||
kkKZ ## "Kazakhstan - Kazakh"
|
||||
klGL ## "Greenland - Greenlandic"
|
||||
kmKH ## "Cambodia - Khmer"
|
||||
knIN ## "India - Kannada"
|
||||
koKR ## "Korea - Korean"
|
||||
kokIN ## "India - Konkani"
|
||||
kyKG ## "Kyrgyzstan - Kyrgyz"
|
||||
lbLU ## "Luxembourg - Luxembourgish"
|
||||
loLA ## "Lao P.D.R. - Lao"
|
||||
ltLT ## "Lithuania - Lithuanian"
|
||||
lvLV ## "Latvia - Latvian"
|
||||
miNZ ## "New Zealand - Maori"
|
||||
mkMK ## "Macedonia (FYROM) - Macedonian (FYROM)"
|
||||
mlIN ## "India - Malayalam"
|
||||
mnCN ## "People's Republic of China - Mongolian (Traditional Mongolian)"
|
||||
mnMN ## "Mongolia - Mongolian (Cyrillic)"
|
||||
mohCA ## "Canada - Mohawk"
|
||||
mrIN ## "India - Marathi"
|
||||
msBN ## "Brunei Darussalam - Malay"
|
||||
msMY ## "Malaysia - Malay"
|
||||
mtMT ## "Malta - Maltese"
|
||||
nbNO ## "Norway - Norwegian (Bokmal)"
|
||||
neNP ## "Nepal - Nepali"
|
||||
nlBE ## "Belgium - Dutch"
|
||||
nlNL ## "Netherlands - Dutch"
|
||||
nnNO ## "Norway - Norwegian (Nynorsk)"
|
||||
nsoZA ## "South Africa - Sesotho sa Leboa"
|
||||
ocFR ## "France - Occitan"
|
||||
orIN ## "India - Oriya"
|
||||
paIN ## "India - Punjabi"
|
||||
plPL ## "Poland - Polish"
|
||||
prsAF ## "Afghanistan - Dari"
|
||||
psAF ## "Afghanistan - Pashto"
|
||||
ptBR ## "Brazil - Portuguese"
|
||||
ptPT ## "Portugal - Portuguese"
|
||||
qutGT ## "Guatemala - K'iche"
|
||||
quzBO ## "Bolivia - Quechua"
|
||||
quzEC ## "Ecuador - Quechua"
|
||||
quzPE ## "Peru - Quechua"
|
||||
rmCH ## "Switzerland - Romansh"
|
||||
roRO ## "Romania - Romanian"
|
||||
ruRU ## "Russia - Russian"
|
||||
rwRW ## "Rwanda - Kinyarwanda"
|
||||
saIN ## "India - Sanskrit"
|
||||
sahRU ## "Russia - Yakut"
|
||||
seFI ## "Finland - Sami (Northern)"
|
||||
seNO ## "Norway - Sami (Northern)"
|
||||
seSE ## "Sweden - Sami (Northern)"
|
||||
siLK ## "Sri Lanka - Sinhala"
|
||||
skSK ## "Slovakia - Slovak"
|
||||
slSI ## "Slovenia - Slovenian"
|
||||
smaNO ## "Norway - Sami (Southern)"
|
||||
smaSE ## "Sweden - Sami (Southern)"
|
||||
smjNO ## "Norway - Sami (Lule)"
|
||||
smjSE ## "Sweden - Sami (Lule)"
|
||||
smnFI ## "Finland - Sami (Inari)"
|
||||
smsFI ## "Finland - Sami (Skolt)"
|
||||
sqAL ## "Albania - Albanian"
|
||||
srBA ## "Bosnia and Herzegovina - Serbian (Cyrillic)"
|
||||
srCS ## "Serbia and Montenegro (Former) - Serbian (Cyrillic)"
|
||||
srME ## "Montenegro - Serbian (Cyrillic)"
|
||||
srRS ## "Serbia - Serbian (Cyrillic)"
|
||||
svFI ## "Finland - Swedish"
|
||||
svSE ## "Sweden - Swedish"
|
||||
swKE ## "Kenya - Kiswahili"
|
||||
syrSY ## "Syria - Syriac"
|
||||
taIN ## "India - Tamil"
|
||||
teIN ## "India - Telugu"
|
||||
tgTJ ## "Tajikistan - Tajik (Cyrillic)"
|
||||
thTH ## "Thailand - Thai"
|
||||
tkTM ## "Turkmenistan - Turkmen"
|
||||
tnZA ## "South Africa - Setswana"
|
||||
trTR ## "Turkey - Turkish"
|
||||
ttRU ## "Russia - Tatar"
|
||||
tzmDZ ## "Algeria - Tamazight (Latin)"
|
||||
ugCN ## "People's Republic of China - Uyghur"
|
||||
ukUA ## "Ukraine - Ukrainian"
|
||||
urPK ## "Islamic Republic of Pakistan - Urdu"
|
||||
uzUZ ## "Uzbekistan - Uzbek (Cyrillic)"
|
||||
viVN ## "Vietnam - Vietnamese"
|
||||
woSN ## "Senegal - Wolof"
|
||||
xhZA ## "South Africa - isiXhosa"
|
||||
yoNG ## "Nigeria - Yoruba"
|
||||
zhCN ## "People's Republic of China - Chinese (Simplified) Legacy"
|
||||
zhHK ## "Hong Kong S.A.R. - Chinese (Traditional) Legacy"
|
||||
zhMO ## "Macao S.A.R. - Chinese (Traditional) Legacy"
|
||||
zhSG ## "Singapore - Chinese (Simplified) Legacy"
|
||||
zhTW ## "Taiwan - Chinese (Traditional) Legacy"
|
||||
zuZA ## "South Africa - isiZulu"
|
||||
|
||||
const
|
||||
languageToName*: array[Language, cstring] = [
|
||||
Language.afZA: cstring"South Africa - Afrikaans",
|
||||
Language.amET: cstring"Ethiopia - Amharic",
|
||||
Language.arAE: cstring"U.A.E. - Arabic",
|
||||
Language.arBH: cstring"Bahrain - Arabic",
|
||||
Language.arDZ: cstring"Algeria - Arabic",
|
||||
Language.arEG: cstring"Egypt - Arabic",
|
||||
Language.arIQ: cstring"Iraq - Arabic",
|
||||
Language.arJO: cstring"Jordan - Arabic",
|
||||
Language.arKW: cstring"Kuwait - Arabic",
|
||||
Language.arLB: cstring"Lebanon - Arabic",
|
||||
Language.arLY: cstring"Libya - Arabic",
|
||||
Language.arMA: cstring"Morocco - Arabic",
|
||||
Language.arOM: cstring"Oman - Arabic",
|
||||
Language.arQA: cstring"Qatar - Arabic",
|
||||
Language.arSA: cstring"Saudi Arabia - Arabic",
|
||||
Language.arSY: cstring"Syria - Arabic",
|
||||
Language.arTN: cstring"Tunisia - Arabic",
|
||||
Language.arYE: cstring"Yemen - Arabic",
|
||||
Language.arnCL: cstring"Chile - Mapudungun",
|
||||
Language.asIN: cstring"India - Assamese",
|
||||
Language.azAZ: cstring"Azerbaijan - Azeri (Cyrillic)",
|
||||
Language.baRU: cstring"Russia - Bashkir",
|
||||
Language.beBY: cstring"Belarus - Belarusian",
|
||||
Language.bgBG: cstring"Bulgaria - Bulgarian",
|
||||
Language.bnBD: cstring"Bangladesh - Bengali",
|
||||
Language.bnIN: cstring"India - Bengali",
|
||||
Language.boCN: cstring"People's Republic of China - Tibetan",
|
||||
Language.brFR: cstring"France - Breton",
|
||||
Language.bsBA: cstring"Bosnia and Herzegovina - Bosnian (Cyrillic)",
|
||||
Language.caES: cstring"Spain - Catalan",
|
||||
Language.coFR: cstring"France - Corsican",
|
||||
Language.csCZ: cstring"Czech Republic - Czech",
|
||||
Language.cyGB: cstring"United Kingdom - Welsh",
|
||||
Language.daDK: cstring"Denmark - Danish",
|
||||
Language.deAT: cstring"Austria - German",
|
||||
Language.deCH: cstring"Switzerland - German",
|
||||
Language.deDE: cstring"Germany - German",
|
||||
Language.deLI: cstring"Liechtenstein - German",
|
||||
Language.deLU: cstring"Luxembourg - German",
|
||||
Language.dsbDE: cstring"Germany - Lower Sorbian",
|
||||
Language.dvMV: cstring"Maldives - Divehi",
|
||||
Language.elGR: cstring"Greece - Greek",
|
||||
Language.en029: cstring"Caribbean - English",
|
||||
Language.enAU: cstring"Australia - English",
|
||||
Language.enBZ: cstring"Belize - English",
|
||||
Language.enCA: cstring"Canada - English",
|
||||
Language.enGB: cstring"United Kingdom - English",
|
||||
Language.enIE: cstring"Ireland - English",
|
||||
Language.enIN: cstring"India - English",
|
||||
Language.enJM: cstring"Jamaica - English",
|
||||
Language.enMY: cstring"Malaysia - English",
|
||||
Language.enNZ: cstring"New Zealand - English",
|
||||
Language.enPH: cstring"Republic of the Philippines - English",
|
||||
Language.enSG: cstring"Singapore - English",
|
||||
Language.enTT: cstring"Trinidad and Tobago - English",
|
||||
Language.enUS: cstring"United States - English",
|
||||
Language.enZA: cstring"South Africa - English",
|
||||
Language.enZW: cstring"Zimbabwe - English",
|
||||
Language.esAR: cstring"Argentina - Spanish",
|
||||
Language.esBO: cstring"Bolivia - Spanish",
|
||||
Language.esCL: cstring"Chile - Spanish",
|
||||
Language.esCO: cstring"Colombia - Spanish",
|
||||
Language.esCR: cstring"Costa Rica - Spanish",
|
||||
Language.esDO: cstring"Dominican Republic - Spanish",
|
||||
Language.esEC: cstring"Ecuador - Spanish",
|
||||
Language.esES: cstring"Spain - Spanish",
|
||||
Language.esGT: cstring"Guatemala - Spanish",
|
||||
Language.esHN: cstring"Honduras - Spanish",
|
||||
Language.esMX: cstring"Mexico - Spanish",
|
||||
Language.esNI: cstring"Nicaragua - Spanish",
|
||||
Language.esPA: cstring"Panama - Spanish",
|
||||
Language.esPE: cstring"Peru - Spanish",
|
||||
Language.esPR: cstring"Puerto Rico - Spanish",
|
||||
Language.esPY: cstring"Paraguay - Spanish",
|
||||
Language.esSV: cstring"El Salvador - Spanish",
|
||||
Language.esUS: cstring"United States - Spanish",
|
||||
Language.esUY: cstring"Uruguay - Spanish",
|
||||
Language.esVE: cstring"Bolivarian Republic of Venezuela - Spanish",
|
||||
Language.etEE: cstring"Estonia - Estonian",
|
||||
Language.euES: cstring"Spain - Basque",
|
||||
Language.faIR: cstring"Iran - Persian",
|
||||
Language.fiFI: cstring"Finland - Finnish",
|
||||
Language.filPH: cstring"Philippines - Filipino",
|
||||
Language.foFO: cstring"Faroe Islands - Faroese",
|
||||
Language.frBE: cstring"Belgium - French",
|
||||
Language.frCA: cstring"Canada - French",
|
||||
Language.frCH: cstring"Switzerland - French",
|
||||
Language.frFR: cstring"France - French",
|
||||
Language.frLU: cstring"Luxembourg - French",
|
||||
Language.frMC: cstring"Principality of Monaco - French",
|
||||
Language.fyNL: cstring"Netherlands - Frisian",
|
||||
Language.gaIE: cstring"Ireland - Irish",
|
||||
Language.gdGB: cstring"United Kingdom - Scottish Gaelic",
|
||||
Language.glES: cstring"Spain - Galician",
|
||||
Language.gswFR: cstring"France - Alsatian",
|
||||
Language.guIN: cstring"India - Gujarati",
|
||||
Language.haNG: cstring"Nigeria - Hausa (Latin)",
|
||||
Language.heIL: cstring"Israel - Hebrew",
|
||||
Language.hiIN: cstring"India - Hindi",
|
||||
Language.hrBA: cstring"Bosnia and Herzegovina - Croatian",
|
||||
Language.hrHR: cstring"Croatia - Croatian",
|
||||
Language.hsbDE: cstring"Germany - Upper Sorbian",
|
||||
Language.huHU: cstring"Hungary - Hungarian",
|
||||
Language.hyAM: cstring"Armenia - Armenian",
|
||||
Language.idID: cstring"Indonesia - Indonesian",
|
||||
Language.igNG: cstring"Nigeria - Igbo",
|
||||
Language.iiCN: cstring"People's Republic of China - Yi",
|
||||
Language.isIS: cstring"Iceland - Icelandic",
|
||||
Language.itCH: cstring"Switzerland - Italian",
|
||||
Language.itIT: cstring"Italy - Italian",
|
||||
Language.iuCA: cstring"Canada - Inuktitut (Latin)",
|
||||
Language.jaJP: cstring"Japan - Japanese",
|
||||
Language.kaGE: cstring"Georgia - Georgian",
|
||||
Language.kkKZ: cstring"Kazakhstan - Kazakh",
|
||||
Language.klGL: cstring"Greenland - Greenlandic",
|
||||
Language.kmKH: cstring"Cambodia - Khmer",
|
||||
Language.knIN: cstring"India - Kannada",
|
||||
Language.koKR: cstring"Korea - Korean",
|
||||
Language.kokIN: cstring"India - Konkani",
|
||||
Language.kyKG: cstring"Kyrgyzstan - Kyrgyz",
|
||||
Language.lbLU: cstring"Luxembourg - Luxembourgish",
|
||||
Language.loLA: cstring"Lao P.D.R. - Lao",
|
||||
Language.ltLT: cstring"Lithuania - Lithuanian",
|
||||
Language.lvLV: cstring"Latvia - Latvian",
|
||||
Language.miNZ: cstring"New Zealand - Maori",
|
||||
Language.mkMK: cstring"Macedonia (FYROM) - Macedonian (FYROM)",
|
||||
Language.mlIN: cstring"India - Malayalam",
|
||||
Language.mnCN: cstring"People's Republic of China - Mongolian (Traditional Mongolian)",
|
||||
Language.mnMN: cstring"Mongolia - Mongolian (Cyrillic)",
|
||||
Language.mohCA: cstring"Canada - Mohawk",
|
||||
Language.mrIN: cstring"India - Marathi",
|
||||
Language.msBN: cstring"Brunei Darussalam - Malay",
|
||||
Language.msMY: cstring"Malaysia - Malay",
|
||||
Language.mtMT: cstring"Malta - Maltese",
|
||||
Language.nbNO: cstring"Norway - Norwegian (Bokmal)",
|
||||
Language.neNP: cstring"Nepal - Nepali",
|
||||
Language.nlBE: cstring"Belgium - Dutch",
|
||||
Language.nlNL: cstring"Netherlands - Dutch",
|
||||
Language.nnNO: cstring"Norway - Norwegian (Nynorsk)",
|
||||
Language.nsoZA: cstring"South Africa - Sesotho sa Leboa",
|
||||
Language.ocFR: cstring"France - Occitan",
|
||||
Language.orIN: cstring"India - Oriya",
|
||||
Language.paIN: cstring"India - Punjabi",
|
||||
Language.plPL: cstring"Poland - Polish",
|
||||
Language.prsAF: cstring"Afghanistan - Dari",
|
||||
Language.psAF: cstring"Afghanistan - Pashto",
|
||||
Language.ptBR: cstring"Brazil - Portuguese",
|
||||
Language.ptPT: cstring"Portugal - Portuguese",
|
||||
Language.qutGT: cstring"Guatemala - K'iche",
|
||||
Language.quzBO: cstring"Bolivia - Quechua",
|
||||
Language.quzEC: cstring"Ecuador - Quechua",
|
||||
Language.quzPE: cstring"Peru - Quechua",
|
||||
Language.rmCH: cstring"Switzerland - Romansh",
|
||||
Language.roRO: cstring"Romania - Romanian",
|
||||
Language.ruRU: cstring"Russia - Russian",
|
||||
Language.rwRW: cstring"Rwanda - Kinyarwanda",
|
||||
Language.saIN: cstring"India - Sanskrit",
|
||||
Language.sahRU: cstring"Russia - Yakut",
|
||||
Language.seFI: cstring"Finland - Sami (Northern)",
|
||||
Language.seNO: cstring"Norway - Sami (Northern)",
|
||||
Language.seSE: cstring"Sweden - Sami (Northern)",
|
||||
Language.siLK: cstring"Sri Lanka - Sinhala",
|
||||
Language.skSK: cstring"Slovakia - Slovak",
|
||||
Language.slSI: cstring"Slovenia - Slovenian",
|
||||
Language.smaNO: cstring"Norway - Sami (Southern)",
|
||||
Language.smaSE: cstring"Sweden - Sami (Southern)",
|
||||
Language.smjNO: cstring"Norway - Sami (Lule)",
|
||||
Language.smjSE: cstring"Sweden - Sami (Lule)",
|
||||
Language.smnFI: cstring"Finland - Sami (Inari)",
|
||||
Language.smsFI: cstring"Finland - Sami (Skolt)",
|
||||
Language.sqAL: cstring"Albania - Albanian",
|
||||
Language.srBA: cstring"Bosnia and Herzegovina - Serbian (Cyrillic)",
|
||||
Language.srCS: cstring"Serbia and Montenegro (Former) - Serbian (Cyrillic)",
|
||||
Language.srME: cstring"Montenegro - Serbian (Cyrillic)",
|
||||
Language.srRS: cstring"Serbia - Serbian (Cyrillic)",
|
||||
Language.svFI: cstring"Finland - Swedish",
|
||||
Language.svSE: cstring"Sweden - Swedish",
|
||||
Language.swKE: cstring"Kenya - Kiswahili",
|
||||
Language.syrSY: cstring"Syria - Syriac",
|
||||
Language.taIN: cstring"India - Tamil",
|
||||
Language.teIN: cstring"India - Telugu",
|
||||
Language.tgTJ: cstring"Tajikistan - Tajik (Cyrillic)",
|
||||
Language.thTH: cstring"Thailand - Thai",
|
||||
Language.tkTM: cstring"Turkmenistan - Turkmen",
|
||||
Language.tnZA: cstring"South Africa - Setswana",
|
||||
Language.trTR: cstring"Turkey - Turkish",
|
||||
Language.ttRU: cstring"Russia - Tatar",
|
||||
Language.tzmDZ: cstring"Algeria - Tamazight (Latin)",
|
||||
Language.ugCN: cstring"People's Republic of China - Uyghur",
|
||||
Language.ukUA: cstring"Ukraine - Ukrainian",
|
||||
Language.urPK: cstring"Islamic Republic of Pakistan - Urdu",
|
||||
Language.uzUZ: cstring"Uzbekistan - Uzbek (Cyrillic)",
|
||||
Language.viVN: cstring"Vietnam - Vietnamese",
|
||||
Language.woSN: cstring"Senegal - Wolof",
|
||||
Language.xhZA: cstring"South Africa - isiXhosa",
|
||||
Language.yoNG: cstring"Nigeria - Yoruba",
|
||||
Language.zhCN: cstring"People's Republic of China - Chinese (Simplified) Legacy",
|
||||
Language.zhHK: cstring"Hong Kong S.A.R. - Chinese (Traditional) Legacy",
|
||||
Language.zhMO: cstring"Macao S.A.R. - Chinese (Traditional) Legacy",
|
||||
Language.zhSG: cstring"Singapore - Chinese (Simplified) Legacy",
|
||||
Language.zhTW: cstring"Taiwan - Chinese (Traditional) Legacy",
|
||||
Language.zuZA: cstring"South Africa - isiZulu"]
|
||||
|
||||
languageToCode*: array[Language, cstring] = [
|
||||
Language.afZA: cstring"af-ZA",
|
||||
Language.amET: cstring"am-ET",
|
||||
Language.arAE: cstring"ar-AE",
|
||||
Language.arBH: cstring"ar-BH",
|
||||
Language.arDZ: cstring"ar-DZ",
|
||||
Language.arEG: cstring"ar-EG",
|
||||
Language.arIQ: cstring"ar-IQ",
|
||||
Language.arJO: cstring"ar-JO",
|
||||
Language.arKW: cstring"ar-KW",
|
||||
Language.arLB: cstring"ar-LB",
|
||||
Language.arLY: cstring"ar-LY",
|
||||
Language.arMA: cstring"ar-MA",
|
||||
Language.arOM: cstring"ar-OM",
|
||||
Language.arQA: cstring"ar-QA",
|
||||
Language.arSA: cstring"ar-SA",
|
||||
Language.arSY: cstring"ar-SY",
|
||||
Language.arTN: cstring"ar-TN",
|
||||
Language.arYE: cstring"ar-YE",
|
||||
Language.arnCL: cstring"arn-CL",
|
||||
Language.asIN: cstring"as-IN",
|
||||
Language.azAZ: cstring"az-AZ",
|
||||
Language.baRU: cstring"ba-RU",
|
||||
Language.beBY: cstring"be-BY",
|
||||
Language.bgBG: cstring"bg-BG",
|
||||
Language.bnBD: cstring"bn-BD",
|
||||
Language.bnIN: cstring"bn-IN",
|
||||
Language.boCN: cstring"bo-CN",
|
||||
Language.brFR: cstring"br-FR",
|
||||
Language.bsBA: cstring"bs-BA",
|
||||
Language.caES: cstring"ca-ES",
|
||||
Language.coFR: cstring"co-FR",
|
||||
Language.csCZ: cstring"cs-CZ",
|
||||
Language.cyGB: cstring"cy-GB",
|
||||
Language.daDK: cstring"da-DK",
|
||||
Language.deAT: cstring"de-AT",
|
||||
Language.deCH: cstring"de-CH",
|
||||
Language.deDE: cstring"de-DE",
|
||||
Language.deLI: cstring"de-LI",
|
||||
Language.deLU: cstring"de-LU",
|
||||
Language.dsbDE: cstring"dsb-DE",
|
||||
Language.dvMV: cstring"dv-MV",
|
||||
Language.elGR: cstring"el-GR",
|
||||
Language.en029: cstring"en-029",
|
||||
Language.enAU: cstring"en-AU",
|
||||
Language.enBZ: cstring"en-BZ",
|
||||
Language.enCA: cstring"en-CA",
|
||||
Language.enGB: cstring"en-GB",
|
||||
Language.enIE: cstring"en-IE",
|
||||
Language.enIN: cstring"en-IN",
|
||||
Language.enJM: cstring"en-JM",
|
||||
Language.enMY: cstring"en-MY",
|
||||
Language.enNZ: cstring"en-NZ",
|
||||
Language.enPH: cstring"en-PH",
|
||||
Language.enSG: cstring"en-SG",
|
||||
Language.enTT: cstring"en-TT",
|
||||
Language.enUS: cstring"en-US",
|
||||
Language.enZA: cstring"en-ZA",
|
||||
Language.enZW: cstring"en-ZW",
|
||||
Language.esAR: cstring"es-AR",
|
||||
Language.esBO: cstring"es-BO",
|
||||
Language.esCL: cstring"es-CL",
|
||||
Language.esCO: cstring"es-CO",
|
||||
Language.esCR: cstring"es-CR",
|
||||
Language.esDO: cstring"es-DO",
|
||||
Language.esEC: cstring"es-EC",
|
||||
Language.esES: cstring"es-ES",
|
||||
Language.esGT: cstring"es-GT",
|
||||
Language.esHN: cstring"es-HN",
|
||||
Language.esMX: cstring"es-MX",
|
||||
Language.esNI: cstring"es-NI",
|
||||
Language.esPA: cstring"es-PA",
|
||||
Language.esPE: cstring"es-PE",
|
||||
Language.esPR: cstring"es-PR",
|
||||
Language.esPY: cstring"es-PY",
|
||||
Language.esSV: cstring"es-SV",
|
||||
Language.esUS: cstring"es-US",
|
||||
Language.esUY: cstring"es-UY",
|
||||
Language.esVE: cstring"es-VE",
|
||||
Language.etEE: cstring"et-EE",
|
||||
Language.euES: cstring"eu-ES",
|
||||
Language.faIR: cstring"fa-IR",
|
||||
Language.fiFI: cstring"fi-FI",
|
||||
Language.filPH: cstring"fil-PH",
|
||||
Language.foFO: cstring"fo-FO",
|
||||
Language.frBE: cstring"fr-BE",
|
||||
Language.frCA: cstring"fr-CA",
|
||||
Language.frCH: cstring"fr-CH",
|
||||
Language.frFR: cstring"fr-FR",
|
||||
Language.frLU: cstring"fr-LU",
|
||||
Language.frMC: cstring"fr-MC",
|
||||
Language.fyNL: cstring"fy-NL",
|
||||
Language.gaIE: cstring"ga-IE",
|
||||
Language.gdGB: cstring"gd-GB",
|
||||
Language.glES: cstring"gl-ES",
|
||||
Language.gswFR: cstring"gsw-FR",
|
||||
Language.guIN: cstring"gu-IN",
|
||||
Language.haNG: cstring"ha-NG",
|
||||
Language.heIL: cstring"he-IL",
|
||||
Language.hiIN: cstring"hi-IN",
|
||||
Language.hrBA: cstring"hr-BA",
|
||||
Language.hrHR: cstring"hr-HR",
|
||||
Language.hsbDE: cstring"hsb-DE",
|
||||
Language.huHU: cstring"hu-HU",
|
||||
Language.hyAM: cstring"hy-AM",
|
||||
Language.idID: cstring"id-ID",
|
||||
Language.igNG: cstring"ig-NG",
|
||||
Language.iiCN: cstring"ii-CN",
|
||||
Language.isIS: cstring"is-IS",
|
||||
Language.itCH: cstring"it-CH",
|
||||
Language.itIT: cstring"it-IT",
|
||||
Language.iuCA: cstring"iu-CA",
|
||||
Language.jaJP: cstring"ja-JP",
|
||||
Language.kaGE: cstring"ka-GE",
|
||||
Language.kkKZ: cstring"kk-KZ",
|
||||
Language.klGL: cstring"kl-GL",
|
||||
Language.kmKH: cstring"km-KH",
|
||||
Language.knIN: cstring"kn-IN",
|
||||
Language.koKR: cstring"ko-KR",
|
||||
Language.kokIN: cstring"kok-IN",
|
||||
Language.kyKG: cstring"ky-KG",
|
||||
Language.lbLU: cstring"lb-LU",
|
||||
Language.loLA: cstring"lo-LA",
|
||||
Language.ltLT: cstring"lt-LT",
|
||||
Language.lvLV: cstring"lv-LV",
|
||||
Language.miNZ: cstring"mi-NZ",
|
||||
Language.mkMK: cstring"mk-MK",
|
||||
Language.mlIN: cstring"ml-IN",
|
||||
Language.mnCN: cstring"mn-CN",
|
||||
Language.mnMN: cstring"mn-MN",
|
||||
Language.mohCA: cstring"moh-CA",
|
||||
Language.mrIN: cstring"mr-IN",
|
||||
Language.msBN: cstring"ms-BN",
|
||||
Language.msMY: cstring"ms-MY",
|
||||
Language.mtMT: cstring"mt-MT",
|
||||
Language.nbNO: cstring"nb-NO",
|
||||
Language.neNP: cstring"ne-NP",
|
||||
Language.nlBE: cstring"nl-BE",
|
||||
Language.nlNL: cstring"nl-NL",
|
||||
Language.nnNO: cstring"nn-NO",
|
||||
Language.nsoZA: cstring"nso-ZA",
|
||||
Language.ocFR: cstring"oc-FR",
|
||||
Language.orIN: cstring"or-IN",
|
||||
Language.paIN: cstring"pa-IN",
|
||||
Language.plPL: cstring"pl-PL",
|
||||
Language.prsAF: cstring"prs-AF",
|
||||
Language.psAF: cstring"ps-AF",
|
||||
Language.ptBR: cstring"pt-BR",
|
||||
Language.ptPT: cstring"pt-PT",
|
||||
Language.qutGT: cstring"qut-GT",
|
||||
Language.quzBO: cstring"quz-BO",
|
||||
Language.quzEC: cstring"quz-EC",
|
||||
Language.quzPE: cstring"quz-PE",
|
||||
Language.rmCH: cstring"rm-CH",
|
||||
Language.roRO: cstring"ro-RO",
|
||||
Language.ruRU: cstring"ru-RU",
|
||||
Language.rwRW: cstring"rw-RW",
|
||||
Language.saIN: cstring"sa-IN",
|
||||
Language.sahRU: cstring"sah-RU",
|
||||
Language.seFI: cstring"se-FI",
|
||||
Language.seNO: cstring"se-NO",
|
||||
Language.seSE: cstring"se-SE",
|
||||
Language.siLK: cstring"si-LK",
|
||||
Language.skSK: cstring"sk-SK",
|
||||
Language.slSI: cstring"sl-SI",
|
||||
Language.smaNO: cstring"sma-NO",
|
||||
Language.smaSE: cstring"sma-SE",
|
||||
Language.smjNO: cstring"smj-NO",
|
||||
Language.smjSE: cstring"smj-SE",
|
||||
Language.smnFI: cstring"smn-FI",
|
||||
Language.smsFI: cstring"sms-FI",
|
||||
Language.sqAL: cstring"sq-AL",
|
||||
Language.srBA: cstring"sr-BA",
|
||||
Language.srCS: cstring"sr-CS",
|
||||
Language.srME: cstring"sr-ME",
|
||||
Language.srRS: cstring"sr-RS",
|
||||
Language.svFI: cstring"sv-FI",
|
||||
Language.svSE: cstring"sv-SE",
|
||||
Language.swKE: cstring"sw-KE",
|
||||
Language.syrSY: cstring"syr-SY",
|
||||
Language.taIN: cstring"ta-IN",
|
||||
Language.teIN: cstring"te-IN",
|
||||
Language.tgTJ: cstring"tg-TJ",
|
||||
Language.thTH: cstring"th-TH",
|
||||
Language.tkTM: cstring"tk-TM",
|
||||
Language.tnZA: cstring"tn-ZA",
|
||||
Language.trTR: cstring"tr-TR",
|
||||
Language.ttRU: cstring"tt-RU",
|
||||
Language.tzmDZ: cstring"tzm-DZ",
|
||||
Language.ugCN: cstring"ug-CN",
|
||||
Language.ukUA: cstring"uk-UA",
|
||||
Language.urPK: cstring"ur-PK",
|
||||
Language.uzUZ: cstring"uz-UZ",
|
||||
Language.viVN: cstring"vi-VN",
|
||||
Language.woSN: cstring"wo-SN",
|
||||
Language.xhZA: cstring"xh-ZA",
|
||||
Language.yoNG: cstring"yo-NG",
|
||||
Language.zhCN: cstring"zh-CN",
|
||||
Language.zhHK: cstring"zh-HK",
|
||||
Language.zhMO: cstring"zh-MO",
|
||||
Language.zhSG: cstring"zh-SG",
|
||||
Language.zhTW: cstring"zh-TW",
|
||||
Language.zuZA: cstring"zu-ZA"]
|
||||
10
karax/localstorage.nim
Normal file
10
karax/localstorage.nim
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
## Karax -- Single page applications for Nim.
|
||||
|
||||
## This module contains wrappers for the HTML 5 local storage.
|
||||
|
||||
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem".}
|
||||
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem".}
|
||||
proc hasItem*(key: cstring): bool {.importcpp: "(localStorage.getItem(#) !== null)".}
|
||||
proc clear*() {.importc: "localStorage.clear".}
|
||||
|
||||
proc removeItem*(key: cstring) {.importc: "localStorage.removeItem".}
|
||||
38
karax/lookuptables.nim
Normal file
38
karax/lookuptables.nim
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
import macros, kbase
|
||||
from strutils import `%`
|
||||
|
||||
macro fieldNamesAsArray*(t: typed; pattern = "$1"): untyped =
|
||||
var impl = getTypeImpl(getTypeImpl(t)[1])
|
||||
if impl.kind == nnkRefTy: impl = getTypeImpl(impl[0])
|
||||
expectKind impl, nnkObjectTy
|
||||
impl = impl[2]
|
||||
expectKind impl, nnkRecList
|
||||
result = newTree(nnkBracket)
|
||||
for x in impl:
|
||||
expectKind x, nnkIdentDefs
|
||||
result.add(newCall(bindSym"kstring", newLit(pattern.strVal % $x[0])))
|
||||
|
||||
proc str(n: NimNode): NimNode =
|
||||
if n.kind in {nnkStrLit, nnkTripleStrLit}:
|
||||
result = newCall(bindSym"kstring", n)
|
||||
else:
|
||||
result = copyNimNode(n)
|
||||
for i in 0..<n.len:
|
||||
result.add str(n[i])
|
||||
|
||||
macro kstrLits*(x: untyped): untyped =
|
||||
## Transforms every string literal "abc" to ``kstring"abc"``. This makes
|
||||
## things much easier to write. String literals of the form ``r"abc"`` are
|
||||
## not affected.
|
||||
result = str(x)
|
||||
|
||||
when isMainModule:
|
||||
type
|
||||
MyObject = ref object
|
||||
x*: string
|
||||
next*: int
|
||||
more, here*: float
|
||||
|
||||
const myObjectFields = fieldNamesAsArray(MyObject, "MyObject_$1")
|
||||
for f in myObjectFields: echo f
|
||||
3
karax/prelude.nim
Normal file
3
karax/prelude.nim
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
## Include file that contains the common imports for the Karax framework.
|
||||
|
||||
import kbase, karax, karaxdsl, vdom, compact, jstrutils
|
||||
380
karax/reactive.nim
Normal file
380
karax/reactive.nim
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
|
||||
import jdict
|
||||
|
||||
type
|
||||
Message = enum
|
||||
Unchanged
|
||||
Changed
|
||||
Mark
|
||||
Inserted
|
||||
Deleted
|
||||
Replaced
|
||||
|
||||
State = object
|
||||
stale: int
|
||||
outdated: bool
|
||||
phantom: bool
|
||||
|
||||
type
|
||||
SinkSeq = seq[proc(msg: Message, pos: int)]
|
||||
ReactiveBase* = ref object of RootObj ## everything that is a "reactive"
|
||||
## value derives from that
|
||||
sinks*: SinkSeq
|
||||
dups: JDict[cstring, int]
|
||||
id: int
|
||||
Reactive*[T] = ref object of ReactiveBase
|
||||
value*: T
|
||||
|
||||
RString* = Reactive[cstring]
|
||||
# RTime* = Reactive[Time]
|
||||
RInt* = Reactive[int]
|
||||
|
||||
RBool* = Reactive[bool]
|
||||
|
||||
RSeq*[T] = ref object of ReactiveBase
|
||||
s: seq[T]
|
||||
L: RInt
|
||||
|
||||
var rid: int
|
||||
|
||||
proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) =
|
||||
if x.dups == nil: x.dups = newJDict[cstring, int]()
|
||||
if not x.dups.contains(key):
|
||||
x.dups[key] = x.sinks.len
|
||||
x.sinks.add sink
|
||||
if x.id == 0:
|
||||
inc rid
|
||||
x.id = rid
|
||||
else:
|
||||
# update existing entry:
|
||||
x.sinks[x.dups[key]] = sink
|
||||
|
||||
proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) =
|
||||
x.sinks.add sink
|
||||
if x.id == 0:
|
||||
inc rid
|
||||
x.id = rid
|
||||
|
||||
var inhibited: int
|
||||
|
||||
proc broadcast(x: ReactiveBase, msg: Message; pos = 0) =
|
||||
if inhibited == 0:
|
||||
for s in x.sinks: s(msg, pos)
|
||||
|
||||
var toTrack: seq[(cstring, proc (msg: Message; pos: int))] = @[]
|
||||
|
||||
template withTrack(key, t, body) =
|
||||
toTrack.add((key, t))
|
||||
body
|
||||
discard toTrack.pop()
|
||||
|
||||
proc trackDependency*(r: ReactiveBase) =
|
||||
for t in toTrack:
|
||||
r.addSink t[0], t[1]
|
||||
|
||||
template wrapObserver(f: untyped) =
|
||||
var state: State
|
||||
proc helper(msg: Message; pos: int) =
|
||||
case msg.kind:
|
||||
of Changed, Unchanged:
|
||||
state.outdated = state.outdated or (msg.kind == Changed)
|
||||
dec state.stale
|
||||
if state.stale == 0:
|
||||
if state.outdated:
|
||||
var t = f()
|
||||
let thisChanged = if x.value != t: Changed else: Unchanged
|
||||
x.value = t
|
||||
x.broadcast(thisChanged, pos)
|
||||
state.outdated = false
|
||||
else:
|
||||
x.broadcast(Unchanged, pos)
|
||||
of Mark:
|
||||
if state.stale == 0: x.broadcast(msg, pos)
|
||||
inc state.stale
|
||||
of Inserted, Deleted, Replaced:
|
||||
dec state.stale
|
||||
if state.stale == 0:
|
||||
x.broadcast(msg, pos)
|
||||
#state.phantom = true
|
||||
helper
|
||||
|
||||
template glitchFree(f: untyped) =
|
||||
var state: State
|
||||
proc helper(msg: Message; pos: int) =
|
||||
case msg.kind:
|
||||
of Changed, Unchanged:
|
||||
state.outdated = state.outdated or (msg.kind == Changed)
|
||||
dec state.stale
|
||||
if state.stale == 0:
|
||||
if state.outdated:
|
||||
x.value = f
|
||||
x.broadcast(msg.kind, pos)
|
||||
state.outdated = false
|
||||
else:
|
||||
x.broadcast(Unchanged, pos)
|
||||
of Mark:
|
||||
if state.stale == 0: x.broadcast(msg, pos)
|
||||
inc state.stale
|
||||
of Inserted, Deleted, Replaced:
|
||||
dec state.stale
|
||||
if state.stale == 0:
|
||||
x.broadcast(msg, pos)
|
||||
helper
|
||||
|
||||
proc `<-`*[T](x: Reactive[T], val: T) =
|
||||
#if x.value != val:
|
||||
x.value = val
|
||||
x.broadcast(Mark)
|
||||
x.broadcast(Changed)
|
||||
|
||||
proc notifyObservers*(x: ReactiveBase) =
|
||||
x.broadcast(Mark)
|
||||
x.broadcast(Changed)
|
||||
|
||||
proc subscribe*[T](x: Reactive[T], f: proc(x: T)) =
|
||||
let reactor = proc (msg: Message, pos: int) =
|
||||
case msg:
|
||||
of Mark: discard
|
||||
of Changed, Unchanged: f(x.value)
|
||||
of Inserted, Deleted, Replaced: discard
|
||||
x.addSink reactor
|
||||
|
||||
proc subscribeSelf*[T: ReactiveBase](x: T, f: proc()) =
|
||||
let reactor = proc (msg: Message, pos: int) =
|
||||
case msg:
|
||||
of Mark: discard
|
||||
of Changed, Unchanged: f()
|
||||
of Inserted, Deleted, Replaced: discard
|
||||
x.addSink reactor
|
||||
|
||||
template lift1(op: untyped) =
|
||||
proc op[T](a: Reactive[T]): (proc(): T) =
|
||||
result = proc(): T = op(a.now)
|
||||
|
||||
template lift2(op: untyped) =
|
||||
proc op[T](a, b: Reactive[T]): (proc(): T) =
|
||||
result = proc(): T = op(a.now, b.now)
|
||||
|
||||
lift1 `not`
|
||||
lift2 `&`
|
||||
|
||||
proc newReactive*[T](x: T): Reactive[T] =
|
||||
result = Reactive[T](value: x)
|
||||
|
||||
proc rstr*(x: cstring): RString =
|
||||
result = RString(value: x)
|
||||
|
||||
proc newRSeq*[T](len: int = 0): RSeq[T] =
|
||||
result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0))
|
||||
|
||||
proc newRSeq*[T](data: seq[T]): RSeq[T] =
|
||||
result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0))
|
||||
for i in 0..high(data):
|
||||
result.s[i] = data[i]
|
||||
|
||||
proc `[]=`*[T](x: RSeq[T]; index: int; v: T) =
|
||||
x.s[index] = v
|
||||
x.broadcast(Mark)
|
||||
x.broadcast(Replaced, index)
|
||||
|
||||
proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index]
|
||||
proc len*[T](x: RSeq[T]): int = x.s.len
|
||||
|
||||
proc add*[T](x: RSeq[T]; y: T) =
|
||||
let position = x.s.len
|
||||
x.s.add(y)
|
||||
x.broadcast(Mark)
|
||||
x.broadcast(Inserted, position)
|
||||
|
||||
proc insert*[T](x: RSeq[T]; y: T; position = 0) =
|
||||
x.s.insert(y, position)
|
||||
x.broadcast(Mark)
|
||||
x.broadcast(Inserted, position)
|
||||
|
||||
proc delete*[T](x: RSeq[T]; position = 0) =
|
||||
x.s.delete(position)
|
||||
x.broadcast(Mark)
|
||||
x.broadcast(Deleted, position)
|
||||
|
||||
proc deleteElem*[T](x: RSeq[T]; y: T) =
|
||||
var position = -1
|
||||
for i in 0..<x.len:
|
||||
if x[i] == y:
|
||||
position = i; break
|
||||
if position >= 0:
|
||||
x.s.delete(position)
|
||||
x.broadcast(Mark)
|
||||
x.broadcast(Deleted, position)
|
||||
|
||||
when false:
|
||||
proc `:=`[T](x: Reactive[T], f: proc(): T) =
|
||||
toTrack = wrapObserver(f())
|
||||
x.value = f()
|
||||
toTrack = nil
|
||||
|
||||
proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] =
|
||||
let xl = x.L.value
|
||||
let res = newRSeq[U](xl)
|
||||
for i in 0..<xl:
|
||||
res.s[i] := proc(): U = f(x[i].now)
|
||||
|
||||
let reactor = proc (msg: Message, pos: int) =
|
||||
case msg:
|
||||
of Mark, Changed, Unchanged: discard "nothing to do"
|
||||
of Inserted:
|
||||
res.insert(f(x[pos]), pos)
|
||||
of Deleted:
|
||||
res.delete(pos)
|
||||
x.addSink reactor
|
||||
result = res
|
||||
|
||||
import macros
|
||||
|
||||
template protect(body: untyped) =
|
||||
#var tmp: seq[proc(msg: Message, pos: int)]
|
||||
#swap(r.sinks, tmp)
|
||||
inc inhibited
|
||||
body
|
||||
dec inhibited
|
||||
#swap(r.sinks, tmp)
|
||||
|
||||
template trackStart(key: cstring; a, b: untyped) =
|
||||
proc differ(m: Message; pos: int) =
|
||||
if m == Changed:
|
||||
protect:
|
||||
karax.runDiff(kxi, a, b)
|
||||
toTrack.add((key, differ))
|
||||
|
||||
template trackEnd() =
|
||||
discard toTrack.pop()
|
||||
|
||||
template doTrack(r: ReactiveBase; a, b: untyped) {.dirty.} =
|
||||
bind addSink, Message, RSeq, Changed, Deleted, Inserted
|
||||
addSink r, proc(m: Message; pos: int) =
|
||||
#when r is RSeq:
|
||||
#echo "Message: ", m, " ", pos
|
||||
if m == Changed:
|
||||
protect r:
|
||||
karax.runDiff(kxi, a, b)
|
||||
|
||||
template doTrackResize(r: ReactiveBase; a, b: untyped) {.dirty.} =
|
||||
bind addSink, Message, RSeq, Changed, Deleted, Inserted
|
||||
addSink r, proc(m: Message; pos: int) =
|
||||
#when r is RSeq:
|
||||
#echo "Message: ", m, " ", pos
|
||||
case m
|
||||
of Deleted: karax.runDel(kxi, a, pos)
|
||||
of Inserted:
|
||||
let it {.used.} = r[pos]
|
||||
karax.runIns(kxi, a, b, pos)
|
||||
else: discard
|
||||
|
||||
macro track*(procDef: untyped): untyped =
|
||||
let params = params(procDef)
|
||||
var trackings = newStmtList()
|
||||
var inner = copyNimTree(procDef)
|
||||
inner[0] = ident($procDef.name & "Inner")
|
||||
var call = newCall(inner[0])
|
||||
|
||||
for j in 1..<params.len:
|
||||
let x = params[j]
|
||||
expectKind x, nnkIdentDefs
|
||||
for i in 0..x.len-3:
|
||||
let param = x[i]
|
||||
call.add(param)
|
||||
|
||||
trackings.add inner
|
||||
let key = newCall("cstring", newLit lineInfo(procDef))
|
||||
trackings.add getAst(trackStart(key, ident"result", call))
|
||||
trackings.add newAssignment(ident"result", call)
|
||||
trackings.add getAst(trackEnd())
|
||||
|
||||
result = copyNimTree(procDef)
|
||||
result.body = trackings
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
|
||||
proc generatePrivateAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
|
||||
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
|
||||
proc name(self: typ): fieldTyp =
|
||||
trackDependency(self)
|
||||
result = self.hidden
|
||||
proc `name=`(self: typ; val: fieldTyp) =
|
||||
self.hidden = val
|
||||
notifyObservers(self)
|
||||
result = getAst(helper(name, hidden, typ, fieldTyp))
|
||||
|
||||
proc generatePublicAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
|
||||
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
|
||||
proc name*(self: typ): fieldTyp =
|
||||
trackDependency(self)
|
||||
result = self.hidden
|
||||
proc `name=`*(self: typ; val: fieldTyp) =
|
||||
self.hidden = val
|
||||
notifyObservers(self)
|
||||
result = getAst(helper(name, hidden, typ, fieldTyp))
|
||||
|
||||
proc transform(n: NimNode; stmts, obj: NimNode): NimNode =
|
||||
if n.kind == nnkIdentDefs and obj.kind == nnkIdent:
|
||||
for i in 0..n.len-3:
|
||||
let it = n[i]
|
||||
let itB = if it.kind == nnkPostFix: it[1] else: it
|
||||
let hidden = newIdentNode("raw" & $itB)
|
||||
if it.kind == nnkPostFix:
|
||||
stmts.add generatePublicAccessors(itB, hidden, obj, n[n.len-2])
|
||||
else:
|
||||
stmts.add generatePrivateAccessors(itB, hidden, obj, n[n.len-2])
|
||||
n[i] = hidden
|
||||
result = n
|
||||
else:
|
||||
var objB = if n.kind == nnkTypeDef: n[0] else: obj
|
||||
result = copyNimNode(n)
|
||||
for i in 0..<n.len:
|
||||
if n.kind == nnkObjectTy and i == 1 and n[1].kind == nnkEmpty:
|
||||
result.add newTree(nnkOfInherit, ident("ReactiveBase"))
|
||||
else:
|
||||
result.add transform(n[i], stmts, objB)
|
||||
|
||||
macro makeReactive*(n: untyped): untyped =
|
||||
var a = newStmtList()
|
||||
a.add newEmptyNode()
|
||||
let t = transform(n, a, newEmptyNode())
|
||||
a[0] = t
|
||||
result = a
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
|
||||
import vdom
|
||||
|
||||
template vmap*(x: RSeq; elem, f: untyped): VNode =
|
||||
let tmp = buildHtml(elem):
|
||||
for i in 0..<len(x):
|
||||
f(x[i])
|
||||
doTrackResize(x, tmp, f(x[pos]))
|
||||
tmp
|
||||
|
||||
template vmapIt*(x: RSeq; elem, call: untyped): VNode =
|
||||
var it {.inject.}: type(x[0])
|
||||
let tmp = buildHtml(elem):
|
||||
for i in 0..<len(x):
|
||||
it = x[i]
|
||||
call
|
||||
doTrackResize(x, tmp, call)
|
||||
tmp
|
||||
|
||||
proc text*(s: RString): VNode =
|
||||
result = text(s.value)
|
||||
s.subscribe proc(v: cstring) =
|
||||
if result.dom != nil: result.dom.nodeValue = v
|
||||
|
||||
when isMainModule:
|
||||
makeReactive:
|
||||
type
|
||||
Foo = ref object of ReactiveBase
|
||||
x, y: int
|
||||
Bar = ref object of ReactiveBase
|
||||
z: string
|
||||
exported*: int
|
||||
Another = ref object
|
||||
a, b: cstring
|
||||
c: bool
|
||||
129
karax/tools/karun.nim
Normal file
129
karax/tools/karun.nim
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
|
||||
import os, strutils, browsers,times, tables
|
||||
import parseopt
|
||||
import threadpool
|
||||
import static_server
|
||||
|
||||
const
|
||||
css = """
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
"""
|
||||
|
||||
const html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<title>$1</title>
|
||||
$2
|
||||
</head>
|
||||
<body id="body" class="site">
|
||||
<div id="ROOT"></div>
|
||||
<script type="text/javascript" src="/app.js"></script>
|
||||
$3
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
const websocket = """
|
||||
<script type="text/javascript">
|
||||
var ws = new WebSocket("ws://localhost:8080/ws");
|
||||
|
||||
ws.onopen = function(evt) {
|
||||
console.log("Connection open ...");
|
||||
ws.send("Hello WebSockets!");
|
||||
};
|
||||
|
||||
ws.onmessage = function(evt) {
|
||||
console.log( "Received Message: " + evt.data);
|
||||
if(evt.data == "refresh"){
|
||||
window.location.href = window.location.href
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function(evt) {
|
||||
console.log("Connection closed.");
|
||||
};
|
||||
</script>
|
||||
"""
|
||||
|
||||
proc exec(cmd: string) =
|
||||
if os.execShellCmd(cmd) != 0:
|
||||
quit "External command failed: " & cmd
|
||||
|
||||
proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
|
||||
echo("Building...")
|
||||
let cmd = "nim js --out:" & "app" & ".js " & rest
|
||||
if watch:
|
||||
discard os.execShellCmd(cmd)
|
||||
else:
|
||||
exec cmd
|
||||
let dest = "app" & ".html"
|
||||
let script = if watch: websocket else: ""
|
||||
writeFile(dest, html % ["app", selectedCss, script])
|
||||
if run: openDefaultBrowser("http://localhost:8080")
|
||||
|
||||
proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
|
||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||
while true:
|
||||
sleep(300)
|
||||
for path in walkDirRec("."):
|
||||
if ".git" in path:
|
||||
continue
|
||||
var (_, _, ext) = splitFile(path)
|
||||
if ext in [".scss",".sass",".less",".styl",".pcss",".postcss"]:
|
||||
continue
|
||||
if files.hasKey(path):
|
||||
if files[path] != getLastModificationTime(path):
|
||||
echo("File changed: " & path)
|
||||
build(rest,selectedCss, false, true)
|
||||
files[path] = getLastModificationTime(path)
|
||||
else:
|
||||
if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
|
||||
continue
|
||||
files[path] = getLastModificationTime(path)
|
||||
|
||||
proc serve(){.thread.} =
|
||||
serveStatic()
|
||||
|
||||
proc main =
|
||||
var op = initOptParser()
|
||||
var rest = op.cmdLineRest
|
||||
var file = ""
|
||||
var run = false
|
||||
var watch = false
|
||||
var selectedCss = ""
|
||||
while true:
|
||||
op.next()
|
||||
case op.kind
|
||||
of cmdLongOption:
|
||||
case op.key
|
||||
of "run":
|
||||
run = true
|
||||
rest = rest.replace("--run ")
|
||||
of "css":
|
||||
if op.val != "":
|
||||
selectedCss = readFile(op.val)
|
||||
else:
|
||||
selectedCss = css
|
||||
rest = rest.substr(rest.find(" "))
|
||||
else: discard
|
||||
of cmdShortOption:
|
||||
if op.key == "r":
|
||||
run = true
|
||||
rest = rest.replace("-r ")
|
||||
if op.key == "w":
|
||||
watch = true
|
||||
rest = rest.replace("-w ")
|
||||
of cmdArgument: file = op.key
|
||||
of cmdEnd: break
|
||||
|
||||
if file.len == 0: quit "filename expected"
|
||||
if run:
|
||||
spawn serve()
|
||||
if watch:
|
||||
spawn watchBuild(file, selectedCss, rest)
|
||||
build(rest,selectedCss, run, watch)
|
||||
sync()
|
||||
|
||||
main()
|
||||
1
karax/tools/karun.nims
Normal file
1
karax/tools/karun.nims
Normal file
|
|
@ -0,0 +1 @@
|
|||
switch("threads", "on")
|
||||
141
karax/tools/static_server.nim
Normal file
141
karax/tools/static_server.nim
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import
|
||||
std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
|
||||
logging, httpcore, asyncfile, asynchttpserver, tables, times]
|
||||
from cgi import decodeUrl
|
||||
import ws, dotenv
|
||||
|
||||
var logger = newConsoleLogger()
|
||||
addHandler(logger)
|
||||
|
||||
when defined(release):
|
||||
setLogFilter(lvlError)
|
||||
|
||||
type
|
||||
RawHeaders* = seq[tuple[key, val: string]]
|
||||
|
||||
proc toStr(headers: RawHeaders): string =
|
||||
$newHttpHeaders(headers)
|
||||
|
||||
proc send(request: Request, code: HttpCode, headers: RawHeaders,
|
||||
body: string): Future[void] =
|
||||
return request.respond(code, body, newHttpHeaders(headers))
|
||||
|
||||
proc statusContent(request: Request, status: HttpCode, content: string,
|
||||
headers: RawHeaders): Future[void] =
|
||||
try:
|
||||
result = send(request, status, headers, content)
|
||||
debug(" ", status, " ", toStr(headers))
|
||||
except:
|
||||
error("Could not send response: ", osErrorMsg(osLastError()))
|
||||
|
||||
proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} =
|
||||
result = Http200
|
||||
let mimes = newMimetypes()
|
||||
for p in paths:
|
||||
if fileExists(p):
|
||||
if fpOthersRead notin getFilePermissions(p):
|
||||
return Http403
|
||||
let fileSize = getFileSize(p)
|
||||
let extPos = searchExtPos(p)
|
||||
let mimetype = mimes.getMimetype(
|
||||
if extPos >= 0: p.substr(extPos + 1)
|
||||
else: "")
|
||||
if fileSize < 10_000_000: # 10 mb
|
||||
var file = readFile(p)
|
||||
var hashed = getMD5(file)
|
||||
# If the user has a cached version of this file and it matches our
|
||||
# version, let them use it
|
||||
if req.headers.getOrDefault("If-None-Match") == hashed:
|
||||
await req.statusContent(Http304, "", default(RawHeaders))
|
||||
else:
|
||||
await req.statusContent(Http200, file, @{
|
||||
"Content-Type": mimetype,
|
||||
"ETag": hashed
|
||||
})
|
||||
else:
|
||||
let headers = @{
|
||||
"Content-Type": mimetype,
|
||||
"Content-Length": $fileSize
|
||||
}
|
||||
await req.statusContent(Http200, "", headers)
|
||||
var fileStream = newFutureStream[string]("sendStaticIfExists")
|
||||
var file = openAsync(p, fmRead)
|
||||
# Let `readToStream` write file data into fileStream in the
|
||||
# background.
|
||||
asyncCheck file.readToStream(fileStream)
|
||||
# The `writeFromStream` proc will complete once all the data in the
|
||||
# `bodyStream` has been written to the file.
|
||||
while true:
|
||||
let (hasValue, value) = await fileStream.read()
|
||||
if hasValue:
|
||||
await req.client.send(value)
|
||||
else:
|
||||
break
|
||||
file.close()
|
||||
return
|
||||
# If we get to here then no match could be found.
|
||||
return Http404
|
||||
|
||||
proc handleFileRequest(req: Request): Future[HttpCode] {.async.} =
|
||||
# Find static file.
|
||||
var reqPath = cgi.decodeUrl(req.url.path)
|
||||
var staticDir = getEnv("staticDir") # it's assumed a relative dir
|
||||
var status = Http400
|
||||
var path = staticDir / reqPath
|
||||
normalizePathEnd(path, false)
|
||||
if dirExists(path):
|
||||
status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"])
|
||||
else:
|
||||
status = await sendStaticIfExists(req, @[path])
|
||||
return status
|
||||
|
||||
proc handleWs(req: Request) {.async.} =
|
||||
var ws = await newWebSocket(req)
|
||||
await ws.send("Welcome to simple echo server")
|
||||
|
||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||
let watchedFiles = [absolutePath "app.js", absolutePath "app.html"]
|
||||
for path in watchedFiles:
|
||||
files[path] = getLastModificationTime(path)
|
||||
|
||||
while ws.readyState == Open:
|
||||
await sleepAsync(500)
|
||||
var changed = false
|
||||
for path in watchedFiles:
|
||||
if files[path] != getLastModificationTime(path):
|
||||
changed = true
|
||||
files[path] = getLastModificationTime(path)
|
||||
if changed:
|
||||
await ws.send("refresh")
|
||||
changed = false
|
||||
|
||||
proc serveStatic*() =
|
||||
if fileExists("static.env"):
|
||||
var env: DotEnv
|
||||
env = initDotEnv(getCurrentDir(), "static.env")
|
||||
env.overload()
|
||||
else:
|
||||
putEnv("staticDir", "assets/")
|
||||
|
||||
var server = newAsyncHttpServer()
|
||||
proc cb(req: Request) {.gcsafe, async.} =
|
||||
if req.url.path == "/ws":
|
||||
await handleWs(req)
|
||||
if req.url.path == "/":
|
||||
await req.respond(Http200, readFile "app.html")
|
||||
elif req.url.path == "/app.js":
|
||||
let file = absolutePath("app" & ".js")
|
||||
if not file.fileExists:
|
||||
error(file, " does not exist!")
|
||||
if fpUserRead notin os.getFilePermissions(file):
|
||||
error("Could not read ", file, "!")
|
||||
await req.respond(Http200, readFile(file))
|
||||
else:
|
||||
let status = await handleFileRequest(req)
|
||||
if status != Http200:
|
||||
await req.respond(status, "")
|
||||
|
||||
waitFor server.serve(Port(8080), cb)
|
||||
|
||||
when isMainModule:
|
||||
serveStatic()
|
||||
490
karax/vdom.nim
Normal file
490
karax/vdom.nim
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
## Virtual DOM implementation for Karax.
|
||||
|
||||
when defined(js):
|
||||
from kdom import Event, Node
|
||||
else:
|
||||
type
|
||||
Event* = ref object
|
||||
Node* = ref object
|
||||
|
||||
import macros, vstyles, kbase
|
||||
from strutils import toUpperAscii, toLowerAscii, tokenize
|
||||
|
||||
type
|
||||
VNodeKind* {.pure.} = enum
|
||||
text = "#text", int = "#int", bool = "#bool",
|
||||
vthunk = "#vthunk", dthunk = "#dthunk",
|
||||
component = "#component", verbatim = "#verbatim",
|
||||
|
||||
html, head, title, base, link, meta, style,
|
||||
script, noscript,
|
||||
body, section, nav, article, aside,
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
header, footer, address, main,
|
||||
|
||||
p, hr, pre, blockquote, ol, ul, li,
|
||||
dl, dt, dd,
|
||||
figure, figcaption,
|
||||
|
||||
tdiv = "div",
|
||||
|
||||
a, em, strong, small,
|
||||
strikethrough = "s", cite, quote,
|
||||
dfn, abbr, data, time, code, `var` = "var", samp,
|
||||
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
|
||||
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
|
||||
ins, del, img, iframe, embed, `object` = "object",
|
||||
param, video, audio, source, track, canvas, map,
|
||||
area, math,
|
||||
|
||||
# SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html
|
||||
animate, animateMotion, animateTransform, circle, clipPath, defs, desc,
|
||||
`discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer,
|
||||
feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap,
|
||||
feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR,
|
||||
feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset,
|
||||
fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence,
|
||||
filter, foreignObject, g, image, line, linearGradient, marker, mask,
|
||||
metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect,
|
||||
`set` = "set", stop, svg, switch, symbol, txt = "text", textPath, tspan,
|
||||
unknown, use, view,
|
||||
|
||||
table, caption, colgroup, col, tbody, thead,
|
||||
tfoot, tr, td, th,
|
||||
|
||||
form, fieldset, legend, label, input, button,
|
||||
select, datalist, optgroup, option, textarea,
|
||||
keygen, output, progress, meter,
|
||||
details, summary, command, menu
|
||||
|
||||
const selfClosing = {area, base, br, col, embed, hr, img, input,
|
||||
link, meta, param, source, track, wbr}
|
||||
|
||||
|
||||
type
|
||||
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
|
||||
onclick, ## An element is clicked.
|
||||
oncontextmenu, ## An element is right-clicked.
|
||||
ondblclick, ## An element is double clicked.
|
||||
onkeyup, ## A key was released.
|
||||
onkeydown, ## A key is pressed.
|
||||
onkeypressed, # A key was pressed.
|
||||
onfocus, ## An element got the focus.
|
||||
onblur, ## An element lost the focus.
|
||||
onchange, ## The selected value of an element was changed.
|
||||
onscroll, ## The user scrolled within an element.
|
||||
|
||||
onmousedown, ## A pointing device button (usually a mouse) is pressed
|
||||
## on an element.
|
||||
onmouseenter, ## A pointing device is moved onto the element that
|
||||
## has the listener attached.
|
||||
onmouseleave, ## A pointing device is moved off the element that
|
||||
## has the listener attached.
|
||||
onmousemove, ## A pointing device is moved over an element.
|
||||
onmouseout, ## A pointing device is moved off the element that
|
||||
## has the listener attached or off one of its children.
|
||||
onmouseover, ## A pointing device is moved onto the element that has
|
||||
## the listener attached or onto one of its children.
|
||||
onmouseup, ## A pointing device button is released over an element.
|
||||
|
||||
ondrag, ## An element or text selection is being dragged (every 350ms).
|
||||
ondragend, ## A drag operation is being ended (by releasing a mouse button
|
||||
## or hitting the escape key).
|
||||
ondragenter, ## A dragged element or text selection enters a valid drop target.
|
||||
ondragleave, ## A dragged element or text selection leaves a valid drop target.
|
||||
ondragover, ## An element or text selection is being dragged over a valid
|
||||
## drop target (every 350ms).
|
||||
ondragstart, ## The user starts dragging an element or text selection.
|
||||
ondrop, ## An element is dropped on a valid drop target.
|
||||
|
||||
onsubmit, ## A form is submitted
|
||||
oninput, ## An input value changes
|
||||
|
||||
onanimationstart,
|
||||
onanimationend,
|
||||
onanimationiteration,
|
||||
|
||||
onkeyupenter, ## vdom extension: an input field received the ENTER key press
|
||||
onkeyuplater, ## vdom extension: a key was pressed and some time
|
||||
## passed (useful for on-the-fly text completions)
|
||||
onload, # img
|
||||
|
||||
ontransitioncancel,
|
||||
ontransitionend,
|
||||
ontransitionrun,
|
||||
ontransitionstart,
|
||||
|
||||
onwheel # fires when the user rotates a wheel button on a pointing device.
|
||||
|
||||
macro buildLookupTables(): untyped =
|
||||
var a = newTree(nnkBracket)
|
||||
for i in low(VNodeKind)..high(VNodeKind):
|
||||
let x = $i
|
||||
let y = if x[0] == '#': x else: toUpperAscii(x)
|
||||
a.add(newCall("kstring", newLit(y)))
|
||||
var e = newTree(nnkBracket)
|
||||
for i in low(EventKind)..high(EventKind):
|
||||
e.add(newCall("kstring", newLit(substr($i, 2))))
|
||||
|
||||
template tmpl(a, e) {.dirty.} =
|
||||
const
|
||||
toTag*: array[VNodeKind, kstring] = a
|
||||
toEventName*: array[EventKind, kstring] = e
|
||||
|
||||
result = getAst tmpl(a, e)
|
||||
|
||||
buildLookupTables()
|
||||
|
||||
type
|
||||
EventHandler* = proc (ev: Event; target: VNode) {.closure.}
|
||||
NativeEventHandler* = proc (ev: Event) {.closure.}
|
||||
|
||||
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
|
||||
|
||||
VKey* = kstring
|
||||
|
||||
VNode* = ref object of RootObj
|
||||
kind*: VNodeKind
|
||||
index*: int ## a generally useful 'index'
|
||||
id*, class*, text*: kstring
|
||||
kids: seq[VNode]
|
||||
# even index: key, odd index: value; done this way for memory efficiency:
|
||||
attrs: seq[kstring]
|
||||
events*: EventHandlers
|
||||
when false:
|
||||
hash*: Hash
|
||||
validHash*: bool
|
||||
style*: VStyle ## the style that should be applied to the virtual node.
|
||||
dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node
|
||||
## is not part of the virtual DOM anymore.
|
||||
|
||||
VComponent* = ref object of VNode ## The abstract class for every karax component.
|
||||
key*: VKey ## key that determines if two components are
|
||||
## identical.
|
||||
renderImpl*: proc(self: VComponent): VNode
|
||||
changedImpl*: proc(self, newInstance: VComponent): bool
|
||||
updatedImpl*: proc(self, newInstance: VComponent)
|
||||
onAttachImpl*: proc(self: VComponent)
|
||||
onDetachImpl*: proc(self: VComponent)
|
||||
version*: int ## Update this to trigger a redraw by karax. Usually you
|
||||
## should call 'markDirty' instead which is an alias for
|
||||
## 'inc version'.
|
||||
renderedVersion*: int ## Do not touch. Used by karax. The last version of the
|
||||
## component we rendered.
|
||||
expanded*: VNode ## Do not touch. Used by karax. The VDOM the component
|
||||
## expanded to.
|
||||
debugId*: int
|
||||
|
||||
proc value*(n: VNode): kstring = n.text
|
||||
proc `value=`*(n: VNode; v: kstring) = n.text = v
|
||||
|
||||
proc intValue*(n: VNode): int = n.index
|
||||
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i)
|
||||
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b))
|
||||
proc vn*(x: kstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
|
||||
|
||||
template callThunk*(fn: typed; n: VNode): untyped =
|
||||
## for internal usage only.
|
||||
fn(n.kids)
|
||||
|
||||
proc vthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
|
||||
VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
|
||||
|
||||
proc dthunk*(dom: Node): VNode =
|
||||
VNode(kind: VNodeKind.dthunk, dom: dom)
|
||||
|
||||
proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
|
||||
assert handler != nil
|
||||
for i in 0..<v.events.len:
|
||||
if v.events[i][0] == kind:
|
||||
#v.events[i][1] = handler
|
||||
return
|
||||
v.events.add((kind, handler, nil))
|
||||
|
||||
proc mergeEvents*(v: VNode; handlers: EventHandlers) =
|
||||
## Overrides or adds the event handlers to `v`'s internal event handler list.
|
||||
for h in handlers: v.setEventIfNoConflict(h[0], h[1])
|
||||
|
||||
proc defaultChangedImpl*(v, newInstance: VComponent): bool =
|
||||
## The default implementation of 'changed'.
|
||||
result = v.key != newInstance.key or v.version != v.renderedVersion
|
||||
|
||||
proc defaultUpdatedImpl*(v, newInstance: VComponent) =
|
||||
discard
|
||||
|
||||
var gid = 0
|
||||
proc getDebugId(): int =
|
||||
inc(gid)
|
||||
gid
|
||||
|
||||
template newComponent*[T](t: typeDesc[T];
|
||||
render: (proc(self: VComponent): VNode) = nil,
|
||||
onAttach: proc(self: VComponent) = nil,
|
||||
onDetach: proc(self: VComponent) = nil,
|
||||
changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl,
|
||||
updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T =
|
||||
## Use this template to create new components.
|
||||
T(kind: VNodeKind.component, index: -1,
|
||||
text: kstring(astToStr(t)), renderImpl: render,
|
||||
changedImpl: changed, updatedImpl: updated,
|
||||
onAttachImpl: onAttach, onDetachImpl: onDetach,
|
||||
debugId: getDebugId())
|
||||
|
||||
template markDirty*(c: VComponent) =
|
||||
## mark the component as dirty so that it is re-rendered.
|
||||
inc c.version
|
||||
|
||||
proc setAttr*(n: VNode; key: kstring; val: kstring = "") =
|
||||
if n.attrs.len == 0:
|
||||
n.attrs = @[key, val]
|
||||
else:
|
||||
for i in countup(0, n.attrs.len-2, 2):
|
||||
if n.attrs[i] == key:
|
||||
n.attrs[i+1] = val
|
||||
return
|
||||
n.attrs.add key
|
||||
n.attrs.add val
|
||||
|
||||
proc getAttr*(n: VNode; key: kstring): kstring =
|
||||
for i in countup(0, n.attrs.len-2, 2):
|
||||
if n.attrs[i] == key: return n.attrs[i+1]
|
||||
|
||||
proc takeOverAttr*(newNode, oldNode: VNode) =
|
||||
shallowCopy oldNode.attrs, newNode.attrs
|
||||
|
||||
proc takeOverFields*(newNode, oldNode: VNode) =
|
||||
template take(field) =
|
||||
shallowCopy oldNode.field, newNode.field
|
||||
take kind
|
||||
take index
|
||||
take id
|
||||
take class
|
||||
take text
|
||||
take kids
|
||||
take attrs
|
||||
take events
|
||||
take style
|
||||
take dom
|
||||
|
||||
proc len*(x: VNode): int = x.kids.len
|
||||
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
|
||||
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
|
||||
|
||||
proc add*(parent, kid: VNode) =
|
||||
when not defined(js) and not defined(nimNoNil):
|
||||
if parent.kids.isNil: parent.kids = @[]
|
||||
parent.kids.add kid
|
||||
|
||||
proc delete*(parent: VNode; position: int) =
|
||||
parent.kids.delete(position)
|
||||
proc insert*(parent, kid: VNode; position: int) =
|
||||
parent.kids.insert(kid, position)
|
||||
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, index: -1)
|
||||
|
||||
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
|
||||
result = newVNode(kind)
|
||||
for k in kids: result.add k
|
||||
|
||||
proc tree*(kind: VNodeKind; attrs: openarray[(kstring, kstring)];
|
||||
kids: varargs[VNode]): VNode =
|
||||
result = tree(kind, kids)
|
||||
for a in attrs: result.setAttr(a[0], a[1])
|
||||
|
||||
when defined(js):
|
||||
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: kstring(s), index: -1)
|
||||
proc text*(s: kstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1)
|
||||
|
||||
when defined(js):
|
||||
proc verbatim*(s: string): VNode =
|
||||
VNode(kind: VNodeKind.verbatim, text: kstring(s), index: -1)
|
||||
proc verbatim*(s: kstring): VNode =
|
||||
VNode(kind: VNodeKind.verbatim, text: s, index: -1)
|
||||
|
||||
|
||||
iterator items*(n: VNode): VNode =
|
||||
for i in 0..<n.kids.len: yield n.kids[i]
|
||||
|
||||
iterator attrs*(n: VNode): (kstring, kstring) =
|
||||
for i in countup(0, n.attrs.len-2, 2):
|
||||
yield (n.attrs[i], n.attrs[i+1])
|
||||
|
||||
proc sameAttrs*(a, b: VNode): bool =
|
||||
if a.attrs.len == b.attrs.len:
|
||||
result = true
|
||||
for i in 0 ..< a.attrs.len:
|
||||
if a.attrs[i] != b.attrs[i]: return false
|
||||
|
||||
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
|
||||
n.events.add((event, handler, nil))
|
||||
|
||||
when kstring is cstring:
|
||||
proc len(a: kstring): int =
|
||||
# xxx: maybe move where kstring is defined
|
||||
# without this, `n.field.len` fails on js (non web) platform
|
||||
if a == nil: 0 else: a.len
|
||||
|
||||
template toStringAttr(field) =
|
||||
if n.field.len > 0:
|
||||
result.add " " & astToStr(field) & " = " & $n.field
|
||||
|
||||
proc toString*(n: VNode; result: var string; indent: int) =
|
||||
for i in 1..indent: result.add ' '
|
||||
if result.len > 0: result.add '\L'
|
||||
result.add "<" & $n.kind
|
||||
toStringAttr(id)
|
||||
toStringAttr(class)
|
||||
for k, v in attrs(n):
|
||||
result.add " " & $k & " = " & $v
|
||||
result.add ">\L"
|
||||
if n.kind == VNodeKind.text:
|
||||
result.add n.text
|
||||
else:
|
||||
if n.text.len > 0:
|
||||
result.add " value = "
|
||||
result.add n.text
|
||||
for child in items(n):
|
||||
toString(child, result, indent+2)
|
||||
for i in 1..indent: result.add ' '
|
||||
result.add "\L</" & $n.kind & ">"
|
||||
|
||||
when false:
|
||||
proc calcHash*(n: VNode) =
|
||||
if n.validHash: return
|
||||
n.validHash = true
|
||||
var h: Hash = ord n.kind
|
||||
if n.id != nil:
|
||||
h &= "id"
|
||||
h &= n.id
|
||||
if n.class != nil:
|
||||
h &= "class"
|
||||
h &= n.class
|
||||
if n.key >= 0:
|
||||
h &= "k"
|
||||
h &= n.key
|
||||
for k, v in attrs(n):
|
||||
h &= " "
|
||||
h &= k
|
||||
h &= "="
|
||||
h &= v
|
||||
if n.kind == VNodeKind.text or n.text != nil:
|
||||
h &= "t"
|
||||
h &= n.text
|
||||
else:
|
||||
for child in items(n):
|
||||
calcHash(child)
|
||||
h &= child.hash
|
||||
n.hash = h
|
||||
|
||||
|
||||
proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
|
||||
## adds the textual representation of `n` to `result`.
|
||||
|
||||
proc addEscapedAttr(result: var string, s: kstring) =
|
||||
# `addEscaped` alternative with less escaped characters.
|
||||
# Only to be used for escaping attribute values enclosed in double quotes!
|
||||
for c in items(s):
|
||||
case c
|
||||
of '<': result.add("<")
|
||||
of '>': result.add(">")
|
||||
of '&': result.add("&")
|
||||
of '"': result.add(""")
|
||||
else: result.add(c)
|
||||
|
||||
proc addEscaped(result: var string, s: kstring) =
|
||||
## same as ``result.add(escape(s))``, but more efficient.
|
||||
for c in items(s):
|
||||
case c
|
||||
of '<': result.add("<")
|
||||
of '>': result.add(">")
|
||||
of '&': result.add("&")
|
||||
of '"': result.add(""")
|
||||
of '\'': result.add("'")
|
||||
of '/': result.add("/")
|
||||
else: result.add(c)
|
||||
|
||||
proc addIndent(result: var string, indent: int) =
|
||||
result.add("\n")
|
||||
for i in 1..indent: result.add(' ')
|
||||
|
||||
if n.kind == VNodeKind.text:
|
||||
result.addEscaped(n.text)
|
||||
elif n.kind == VNodeKind.verbatim:
|
||||
result.add(n.text)
|
||||
else:
|
||||
let kind = $n.kind
|
||||
result.add('<')
|
||||
result.add(kind)
|
||||
if n.id.len > 0:
|
||||
result.add " id=\""
|
||||
result.addEscapedAttr(n.id)
|
||||
result.add('"')
|
||||
if n.class.len > 0:
|
||||
result.add " class=\""
|
||||
result.addEscapedAttr(n.class)
|
||||
result.add('"')
|
||||
for k, v in attrs(n):
|
||||
result.add(' ')
|
||||
result.add(k)
|
||||
result.add("=\"")
|
||||
result.addEscapedAttr(v)
|
||||
result.add('"')
|
||||
if n.style != nil:
|
||||
result.add " style=\""
|
||||
for k, v in pairs(n.style):
|
||||
if v.len == 0: continue
|
||||
for t in tokenize($k, seps={'A' .. 'Z'}):
|
||||
if t.isSep: result.add '-'
|
||||
result.add toLowerAscii(t.token)
|
||||
result.add ": "
|
||||
result.add v
|
||||
result.add "; "
|
||||
result.add('"')
|
||||
if n.len > 0:
|
||||
result.add('>')
|
||||
if n.len > 1:
|
||||
var noWhitespace = false
|
||||
for i in 0..<n.len:
|
||||
if n[i].kind == VNodeKind.text:
|
||||
noWhitespace = true
|
||||
break
|
||||
|
||||
if noWhitespace:
|
||||
# for mixed leaves, we cannot output whitespace for readability,
|
||||
# because this would be wrong. For example: ``a<b>b</b>`` is
|
||||
# different from ``a <b>b</b>``.
|
||||
for i in 0..<n.len: result.add(n[i], indent+indWidth, indWidth)
|
||||
else:
|
||||
for i in 0..<n.len:
|
||||
result.addIndent(indent+indWidth)
|
||||
result.add(n[i], indent+indWidth, indWidth)
|
||||
result.addIndent(indent)
|
||||
else:
|
||||
result.add(n[0], indent+indWidth, indWidth)
|
||||
result.add("</")
|
||||
result.add(kind)
|
||||
result.add(">")
|
||||
elif n.kind in selfClosing:
|
||||
result.add(" />")
|
||||
else:
|
||||
result.add(">")
|
||||
result.add("</")
|
||||
result.add(kind)
|
||||
result.add(">")
|
||||
|
||||
|
||||
proc `$`*(n: VNode): kstring =
|
||||
when defined(js):
|
||||
var res = ""
|
||||
toString(n, res, 0)
|
||||
result = kstring(res)
|
||||
else:
|
||||
result = ""
|
||||
add(result, n)
|
||||
|
||||
proc getVNodeById*(n: VNode; id: cstring): VNode =
|
||||
## Get the VNode that was marked with ``id``. Returns ``nil``
|
||||
## if no node exists.
|
||||
if n.id == id: return n
|
||||
for i in 0..<n.len:
|
||||
result = getVNodeById(n[i], id)
|
||||
if result != nil: return result
|
||||
354
karax/vstyles.nim
Normal file
354
karax/vstyles.nim
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
##[
|
||||
see examples/hellostyle.nim
|
||||
]##
|
||||
|
||||
import std/[macros, strutils]
|
||||
import kbase
|
||||
|
||||
when defined(js):
|
||||
import kdom, jdict
|
||||
|
||||
type
|
||||
StyleAttr* {.pure.} = enum
|
||||
## The style attributes supported by the virtual DOM.
|
||||
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
|
||||
alignContent
|
||||
alignItems
|
||||
alignSelf
|
||||
animation
|
||||
animationDelay
|
||||
animationDirection
|
||||
animationDuration
|
||||
animationFillMode
|
||||
animationIterationCount
|
||||
animationName
|
||||
animationTimingFunction
|
||||
animationPlayState
|
||||
background
|
||||
backgroundAttachment
|
||||
backgroundColor
|
||||
backgroundImage
|
||||
backgroundPosition
|
||||
backgroundRepeat
|
||||
backgroundClip
|
||||
backgroundOrigin
|
||||
backgroundSize
|
||||
backfaceVisibility
|
||||
border
|
||||
borderBottom
|
||||
borderBottomColor
|
||||
borderBottomLeftRadius
|
||||
borderBottomRightRadius
|
||||
borderBottomStyle
|
||||
borderBottomWidth
|
||||
borderCollapse
|
||||
borderColor
|
||||
borderImage
|
||||
borderImageOutset
|
||||
borderImageRepeat
|
||||
borderImageSlice
|
||||
borderImageSource
|
||||
borderImageWidth
|
||||
borderLeft
|
||||
borderLeftColor
|
||||
borderLeftStyle
|
||||
borderLeftWidth
|
||||
borderRadius
|
||||
borderRight
|
||||
borderRightColor
|
||||
borderRightStyle
|
||||
borderRightWidth
|
||||
borderSpacing
|
||||
borderStyle
|
||||
borderTop
|
||||
borderTopColor
|
||||
borderTopLeftRadius
|
||||
borderTopRightRadius
|
||||
borderTopStyle
|
||||
borderTopWidth
|
||||
borderWidth
|
||||
bottom
|
||||
boxDecorationBreak
|
||||
boxShadow
|
||||
boxSizing
|
||||
captionSide
|
||||
clear
|
||||
clip
|
||||
color
|
||||
columnCount
|
||||
columnFill
|
||||
columnGap
|
||||
columnRule
|
||||
columnRuleColor
|
||||
columnRuleStyle
|
||||
columnRuleWidth
|
||||
columns
|
||||
columnSpan
|
||||
columnWidth
|
||||
content
|
||||
counterIncrement
|
||||
counterReset
|
||||
cursor
|
||||
direction
|
||||
display
|
||||
emptyCells
|
||||
filter
|
||||
flex
|
||||
flexBasis
|
||||
flexDirection
|
||||
flexFlow
|
||||
flexGrow
|
||||
flexShrink
|
||||
flexWrap
|
||||
cssFloat
|
||||
font
|
||||
fontFamily
|
||||
fontSize
|
||||
fontSizeAdjust
|
||||
fontStretch
|
||||
fontStyle
|
||||
fontVariant
|
||||
fontWeight
|
||||
hangingPunctuation
|
||||
height
|
||||
hyphens
|
||||
icon
|
||||
imageOrientation
|
||||
justifyContent
|
||||
left
|
||||
letterSpacing
|
||||
lineHeight
|
||||
listStyle
|
||||
listStyleImage
|
||||
listStylePosition
|
||||
listStyleType
|
||||
margin
|
||||
marginBottom
|
||||
marginLeft
|
||||
marginRight
|
||||
marginTop
|
||||
maxHeight
|
||||
maxWidth
|
||||
minHeight
|
||||
minWidth
|
||||
navDown
|
||||
navIndex
|
||||
navLeft
|
||||
navRight
|
||||
navUp
|
||||
opacity
|
||||
order
|
||||
orphans
|
||||
outline
|
||||
outlineColor
|
||||
outlineOffset
|
||||
outlineStyle
|
||||
outlineWidth
|
||||
overflow
|
||||
overflowX
|
||||
overflowY
|
||||
padding
|
||||
paddingBottom
|
||||
paddingLeft
|
||||
paddingRight
|
||||
paddingTop
|
||||
pageBreakAfter
|
||||
pageBreakBefore
|
||||
pageBreakInside
|
||||
perspective
|
||||
perspectiveOrigin
|
||||
pointerEvents
|
||||
position
|
||||
quotes
|
||||
resize
|
||||
right
|
||||
scrollbar3dLightColor # note: missing in w3schools reference
|
||||
scrollbarArrowColor # note: missing in w3schools reference
|
||||
scrollbarBaseColor # note: missing in w3schools reference
|
||||
scrollbarDarkshadowColor # note: missing in w3schools reference
|
||||
scrollbarFaceColor # note: missing in w3schools reference
|
||||
scrollbarHighlightColor # note: missing in w3schools reference
|
||||
scrollbarShadowColor # note: missing in w3schools reference
|
||||
scrollbarTrackColor # note: missing in w3schools reference
|
||||
tableLayout
|
||||
tabSize
|
||||
textAlign
|
||||
textAlignLast
|
||||
textDecoration
|
||||
textDecorationColor
|
||||
textDecorationLine
|
||||
textDecorationStyle
|
||||
textIndent
|
||||
textJustify
|
||||
textOverflow
|
||||
textShadow
|
||||
textTransform
|
||||
top
|
||||
transform
|
||||
transformOrigin
|
||||
transformStyle
|
||||
transition
|
||||
transitionDelay
|
||||
transitionDuration
|
||||
transitionProperty
|
||||
transitionTimingFunction
|
||||
unicodeBidi
|
||||
userSelect
|
||||
verticalAlign
|
||||
visibility
|
||||
whiteSpace
|
||||
width
|
||||
wordBreak
|
||||
wordSpacing
|
||||
wordWrap
|
||||
widows
|
||||
zIndex
|
||||
|
||||
macro buildLookupTables(): untyped =
|
||||
var e = newTree(nnkBracket)
|
||||
for i in low(StyleAttr)..high(StyleAttr):
|
||||
e.add(newCall("kstring", newLit($i)))
|
||||
template tmpl(e) {.dirty.} =
|
||||
const
|
||||
toStyleAttrName: array[StyleAttr, kstring] = e
|
||||
result = getAst tmpl(e)
|
||||
|
||||
buildLookupTables()
|
||||
|
||||
# I optimized the heck out of this representation since profiling showed
|
||||
# it's relevant.
|
||||
# even index: key, odd index: value; done this way for memory efficiency:
|
||||
|
||||
when defined(js):
|
||||
type
|
||||
VStyle* = JSeq[cstring]
|
||||
else:
|
||||
type
|
||||
VStyle* = ref seq[string]
|
||||
proc len*(a: VStyle): int = len(a[])
|
||||
proc add*(a: VStyle; x: string) = add(a[], x)
|
||||
|
||||
proc eq*(a, b: VStyle): bool =
|
||||
if a.isNil:
|
||||
if b.isNil: return true
|
||||
else: return false
|
||||
elif b.isNil: return false
|
||||
if a.len != b.len: return false
|
||||
for i in 0..<a.len:
|
||||
if a[i] != b[i]: return false
|
||||
return true
|
||||
|
||||
proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
|
||||
## inserts (a, value) in sorted order of key `a`
|
||||
# worst case quadratic complexity (if given styles in reverse order), hopefully
|
||||
# not a concern assuming small cardinal
|
||||
var i = 0
|
||||
while i < s.len:
|
||||
if s[i] == a:
|
||||
s[i+1] = value
|
||||
return
|
||||
elif s[i] > a:
|
||||
s.add ""
|
||||
s.add ""
|
||||
# insertion point here, shift all remaining pairs by 2 indexes
|
||||
for j in countdown(s.len-1, i+3, 2):
|
||||
s[j] = s[j-2]
|
||||
s[j-1] = s[j-3]
|
||||
s[i] = a
|
||||
s[i+1] = value
|
||||
return
|
||||
inc i, 2
|
||||
s.add a
|
||||
s.add value
|
||||
|
||||
proc setAttr*(s: VStyle; attr: StyleAttr, value: kstring) {.noSideEffect.} =
|
||||
when kstring is cstring:
|
||||
assert value != nil, "value must not be nil"
|
||||
setAttr(s, toStyleAttrName[attr], value)
|
||||
|
||||
proc getAttr*(s: VStyle; attr: StyleAttr): kstring {.noSideEffect.} =
|
||||
## returns "" if the attribute has not been set.
|
||||
var i = 0
|
||||
let a = toStyleAttrName[attr]
|
||||
while i < s.len:
|
||||
if s[i] == a:
|
||||
return s[i+1]
|
||||
elif s[i] > a:
|
||||
return ""
|
||||
inc i, 2
|
||||
|
||||
proc style*(pairs: varargs[(StyleAttr, kstring)]): VStyle {.noSideEffect.} =
|
||||
## constructs a VStyle object from a list of (attribute, value)-pairs.
|
||||
when defined(js):
|
||||
result = newJSeq[cstring]()
|
||||
else:
|
||||
new(result)
|
||||
result[] = @[]
|
||||
for x in pairs:
|
||||
result.setAttr x[0], x[1]
|
||||
|
||||
proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
|
||||
## constructs a VStyle object from a single (attribute, value)-pair.
|
||||
when defined(js):
|
||||
result = newJSeq[cstring]()
|
||||
else:
|
||||
new(result)
|
||||
result[] = @[]
|
||||
result.setAttr a, val
|
||||
|
||||
proc toCss*(a: string): VStyle =
|
||||
##[
|
||||
See example in hellostyle.nim
|
||||
Allows passing a css string directly, eg:
|
||||
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard
|
||||
tdiv(style = "font-style: oblique; color: pink".toCss): discard
|
||||
]##
|
||||
when defined(js):
|
||||
result = newJSeq[cstring]()
|
||||
else:
|
||||
new(result)
|
||||
result[] = @[]
|
||||
for ai in a.split(";"):
|
||||
var ai = ai.strip
|
||||
if ai.len == 0: continue
|
||||
let aj = ai.strip.split(":", maxsplit=1)
|
||||
result.setAttr(aj[0], aj[1])
|
||||
|
||||
when defined(js):
|
||||
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
|
||||
|
||||
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
|
||||
## apply the style to the real DOM node ``n``.
|
||||
|
||||
#n.style = Style() # optimized, this is a hotspot:
|
||||
{.emit: "`n`.style = {};".}
|
||||
for i in countup(0, s.len-1, 2):
|
||||
n.style.setStyle(s[i], s[i+1])
|
||||
|
||||
proc merge*(a, b: VStyle): VStyle {.noSideEffect.} =
|
||||
## merges two styles. ``b`` takes precedence over ``a``.
|
||||
when defined(js):
|
||||
result = newJSeq[cstring]()
|
||||
else:
|
||||
new(result)
|
||||
result[] = @[]
|
||||
for i in 0..<a.len:
|
||||
result.add a[i]
|
||||
for i in countup(0, b.len-1, 2):
|
||||
setAttr(result, b[i], b[i+1])
|
||||
|
||||
iterator pairs*(s: VStyle): (kstring, kstring) {.noSideEffect.} =
|
||||
if s != nil:
|
||||
for i in countup(0, s.len-1, 2):
|
||||
yield (s[i], s[i+1])
|
||||
|
||||
when defined(js):
|
||||
import jstrutils
|
||||
else:
|
||||
template `&`(x: untyped): untyped = $x
|
||||
|
||||
proc rgb*(r, g, b: range[0..255]): kstring =
|
||||
kstring"rgb(" & &r & kstring", " & &g & kstring", " & &b & kstring")"
|
||||
|
||||
proc rgba*(r, g, b: range[0..255], alpha: float): kstring =
|
||||
kstring"rgba(" & &r & kstring", " & &g & kstring", " & &b & kstring", " & &alpha & kstring")"
|
||||
394
readme.md
Normal file
394
readme.md
Normal file
|
|
@ -0,0 +1,394 @@
|
|||

|
||||
|
||||
    
|
||||
|
||||
# Karax
|
||||
Karax is a framework for developing single page applications in Nim.
|
||||
|
||||
## Install
|
||||
|
||||
To use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html).
|
||||
|
||||
Then you can install karax through nimble:
|
||||
``nimble install karax``
|
||||
|
||||
## Try Karax
|
||||
To try it out, run:
|
||||
|
||||
``cd ~/projects # Insert your favourite directory for projects``
|
||||
|
||||
``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble``
|
||||
|
||||
``cd karax``
|
||||
|
||||
``cd examples/todoapp``
|
||||
|
||||
``nim js todoapp.nim``
|
||||
|
||||
``open todoapp.html``
|
||||
|
||||
``cd ../..``
|
||||
|
||||
``cd examples/mediaplayer``
|
||||
|
||||
``nim js playerapp.nim``
|
||||
|
||||
``open playerapp.html``
|
||||
|
||||
It uses a virtual DOM like React, but is much smaller than the existing
|
||||
frameworks plus of course it's written in Nim for Nim. No external
|
||||
dependencies! And thanks to Nim's whole program optimization only what
|
||||
is used ends up in the generated JavaScript code.
|
||||
|
||||
|
||||
## Goals
|
||||
|
||||
|
||||
- Leverage Nim's macro system to produce a framework that allows
|
||||
for the development of applications that are boilerplate free.
|
||||
- Keep it small, keep it fast, keep it flexible.
|
||||
|
||||
|
||||
|
||||
## Hello World
|
||||
|
||||
|
||||
The simplest Karax program looks like this:
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
text "Hello World!"
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
|
||||
Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
|
||||
here. ``tdiv`` produces a ``<div>`` virtual DOM node.
|
||||
|
||||
As you can see, karax comes with its own ``buildHtml`` DSL for convenient
|
||||
construction of (virtual) DOM trees (of type ``VNode``). Karax provides
|
||||
a tiny build tool called ``karun`` that generates the HTML boilerplate code that
|
||||
embeds and invokes the generated JavaScript code::
|
||||
|
||||
``nim c karax/tools/karun``
|
||||
``karax/tools/karun -r helloworld.nim``
|
||||
|
||||
Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
|
||||
``buildHtml``:
|
||||
|
||||
```nim
|
||||
|
||||
let tmp1 = tree(VNodeKind.tdiv)
|
||||
add(tmp1, text "Hello World!")
|
||||
tmp1
|
||||
```
|
||||
(I shortened the IDs for better readability.)
|
||||
|
||||
Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
|
||||
construction so that it composes with all of Nim's control flow constructs:
|
||||
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
import random
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if rand(100) <= 50:
|
||||
text "Hello World!"
|
||||
else:
|
||||
text "Hello Universe"
|
||||
|
||||
randomize()
|
||||
setRenderer createDom
|
||||
|
||||
```
|
||||
Produces:
|
||||
|
||||
```nim
|
||||
|
||||
let tmp1 = tree(VNodeKind.tdiv)
|
||||
if rand(100) <= 50:
|
||||
add(tmp1, text "Hello World!")
|
||||
else:
|
||||
add(tmp1, text "Hello Universe")
|
||||
tmp1
|
||||
```
|
||||
|
||||
## Event model
|
||||
|
||||
Karax does not change the DOM's event model much, here is a program
|
||||
that writes "Hello simulated universe" on a button click:
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button:
|
||||
text "Say hello!"
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
lines.add "Hello simulated universe"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
|
||||
``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
|
||||
string"; for the JS target that is an immutable JavaScript string) which
|
||||
is preferred for efficiency on the JS target. However, on the native targets
|
||||
``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
|
||||
construction is also avaible for the native targets (!) and the ``kstring``
|
||||
abstraction helps to deal with these conflicting requirements.
|
||||
|
||||
Karax's DSL is quite flexible when it comes to event handlers, so the
|
||||
following syntax is also supported:
|
||||
|
||||
```nim
|
||||
|
||||
include karax / prelude
|
||||
from sugar import `=>`
|
||||
|
||||
var lines: seq[kstring] = @[]
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
button(onclick = () => lines.add "Hello simulated universe"):
|
||||
text "Say hello!"
|
||||
for x in lines:
|
||||
tdiv:
|
||||
text x
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
|
||||
The ``buildHtml`` macro produces this code for us:
|
||||
|
||||
```nim
|
||||
|
||||
let tmp2 = tree(VNodeKind.tdiv)
|
||||
let tmp3 = tree(VNodeKind.button)
|
||||
addEventHandler(tmp3, EventKind.onclick,
|
||||
() => lines.add "Hello simulated universe", kxi)
|
||||
add(tmp3, text "Say hello!")
|
||||
add(tmp2, tmp3)
|
||||
for x in lines:
|
||||
let tmp4 = tree(VNodeKind.tdiv)
|
||||
add(tmp4, text x)
|
||||
add(tmp2, tmp4)
|
||||
tmp2
|
||||
```
|
||||
As the examples grow larger it becomes more and more visible of what
|
||||
a DSL that composes with the builtin Nim control flow constructs buys us.
|
||||
Once you have tasted this power there is no going back and languages
|
||||
without AST based macro system simply don't cut it anymore.
|
||||
|
||||
|
||||
## Attaching data to an event handler
|
||||
|
||||
|
||||
Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
|
||||
additional data that should be passed to the event handler needs to be
|
||||
done via Nim's closures. In general this means a pattern like this:
|
||||
|
||||
```nim
|
||||
|
||||
proc menuAction(menuEntry: kstring): proc() =
|
||||
result = proc() =
|
||||
echo "clicked ", menuEntry
|
||||
|
||||
proc buildMenu(menu: seq[kstring]): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
for m in menu:
|
||||
nav(class="navbar is-primary"):
|
||||
tdiv(class="navbar-brand"):
|
||||
a(class="navbar-item", onclick = menuAction(m)):
|
||||
```
|
||||
|
||||
## DOM diffing
|
||||
|
||||
Ok, so now we have seen DOM creation and event handlers. But how does
|
||||
Karax actually keep the DOM up to date? The trick is that every event
|
||||
handler is wrapped in a helper proc that triggers a *redraw* operation
|
||||
that calls the *renderer* that you initially passed to ``setRenderer``.
|
||||
So a new virtual DOM is created and compared against the previous
|
||||
virtual DOM. This comparison produces a patch set that is then applied
|
||||
to the real DOM the browser uses internally. This process is called
|
||||
"virtual DOM diffing" and other frameworks, most notably Facebook's
|
||||
*React*, do quite similar things. The virtual DOM is faster to create
|
||||
and manipulate than the real DOM so this approach is quite efficient.
|
||||
|
||||
|
||||
## Form validation
|
||||
Most applications these days have some "login"
|
||||
mechanism consisting of ``username`` and ``password`` and
|
||||
a ``login`` button. The login button should only be clickable
|
||||
if ``username`` and ``password`` are not empty. An error
|
||||
message should be shown as long as one input field is empty.
|
||||
|
||||
To create new UI elements we write a ``loginField`` proc that
|
||||
returns a ``VNode``:
|
||||
|
||||
```nim
|
||||
|
||||
proc loginField(desc, field, class: kstring;
|
||||
validator: proc (field: kstring): proc ()): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
label(`for` = field):
|
||||
text desc
|
||||
input(class = class, id = field, onchange = validator(field))
|
||||
```
|
||||
|
||||
We use the ``karax / errors`` module to help with this error
|
||||
logic. The ``errors`` module is mostly a mapping from strings to
|
||||
strings but it turned out that the logic is tricky enough to warrant
|
||||
a library solution. ``validateNotEmpty`` returns a closure that
|
||||
captures the ``field`` parameter:
|
||||
|
||||
```nim
|
||||
|
||||
proc validateNotEmpty(field: kstring): proc () =
|
||||
result = proc () =
|
||||
let x = getVNodeById(field).getInputText
|
||||
if x.isNil or x == "":
|
||||
errors.setError(field, field & " must not be empty")
|
||||
else:
|
||||
errors.setError(field, "")
|
||||
```
|
||||
|
||||
This indirection is required because
|
||||
event handlers in Karax need to have the type ``proc ()``
|
||||
or ``proc (ev: Event; n: VNode)``. The errors module also
|
||||
gives us a handy ``disableOnError`` helper. It returns
|
||||
``"disabled"`` if there are errors. Now we have all the
|
||||
pieces together to write our login dialog:
|
||||
|
||||
|
||||
```nim
|
||||
|
||||
# some consts in order to prevent typos:
|
||||
const
|
||||
username = kstring"username"
|
||||
password = kstring"password"
|
||||
|
||||
var loggedIn: bool
|
||||
|
||||
proc loginDialog(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
if not loggedIn:
|
||||
loginField("Name :", username, "input", validateNotEmpty)
|
||||
loginField("Password: ", password, "password", validateNotEmpty)
|
||||
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
|
||||
text "Login"
|
||||
p:
|
||||
text errors.getError(username)
|
||||
p:
|
||||
text errors.getError(password)
|
||||
else:
|
||||
p:
|
||||
text "You are now logged in."
|
||||
|
||||
setRenderer loginDialog
|
||||
```
|
||||
|
||||
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).)
|
||||
|
||||
This code still has a bug though, when you run it, the ``login`` button is not
|
||||
disabled until some input fields are validated! This is easily fixed,
|
||||
at initialization we have to do:
|
||||
|
||||
```nim
|
||||
|
||||
setError username, username & " must not be empty"
|
||||
setError password, password & " must not be empty"
|
||||
```
|
||||
There are likely more elegant solutions to this problem.
|
||||
|
||||
## Routing
|
||||
|
||||
|
||||
For routing ``setRenderer`` can be called with a callback that takes a parameter of
|
||||
type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
|
||||
|
||||
```nim
|
||||
|
||||
proc createDom(data: RouterData): VNode =
|
||||
if data.hashPart == "#/": filter = all
|
||||
elif data.hashPart == "#/completed": filter = completed
|
||||
elif data.hashPart == "#/active": filter = active
|
||||
result = buildHtml(tdiv(class="todomvc-wrapper")):
|
||||
section(class = "todoapp"):
|
||||
...
|
||||
|
||||
setRenderer createDom
|
||||
```
|
||||
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).)
|
||||
|
||||
## Server Side HTML Rendering
|
||||
|
||||
Karax can also be used to render HTML on the server. Only a subset of
|
||||
modules can be used since there is no JS interpreter.
|
||||
|
||||
```nim
|
||||
|
||||
import karax / [karaxdsl, vdom]
|
||||
|
||||
const places = @["boston", "cleveland", "los angeles", "new orleans"]
|
||||
|
||||
proc render*(): string =
|
||||
let vnode = buildHtml(tdiv(class = "mt-3")):
|
||||
h1: text "My Web Page"
|
||||
p: text "Hello world"
|
||||
ul:
|
||||
for place in places:
|
||||
li: text place
|
||||
dl:
|
||||
dt: text "Can I use Karax for client side single page apps?"
|
||||
dd: text "Yes"
|
||||
|
||||
dt: text "Can I use Karax for server side HTML rendering?"
|
||||
dd: text "Yes"
|
||||
result = $vnode
|
||||
|
||||
echo render()
|
||||
```
|
||||
## Generate HTML with event handlers
|
||||
|
||||
If you are writing a static site generator or do server-side HTML rendering
|
||||
via ``nim c``, you may want to override ``addEventHandler`` when using event
|
||||
handlers to avoid compiler complaints.
|
||||
|
||||
Here's an example of auto submit a dropdown when a value is selected:
|
||||
|
||||
```nim
|
||||
|
||||
template kxi(): int = 0
|
||||
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
|
||||
n.setAttr($k, action)
|
||||
|
||||
let
|
||||
names = @["nim", "c", "python"]
|
||||
selected_name = request.params.getOrDefault("name")
|
||||
hello = buildHtml(html):
|
||||
form(`method` = "get"):
|
||||
select(name="name", onchange="this.form.submit()"):
|
||||
for name in names:
|
||||
if name == selected_name:
|
||||
option(selected = ""): text name
|
||||
else:
|
||||
option: text name
|
||||
```
|
||||
|
||||
## License
|
||||
MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt).
|
||||
26
readme.rst
26
readme.rst
|
|
@ -1,26 +0,0 @@
|
|||
Karax -- Single page applications in Nim
|
||||
========================================
|
||||
|
||||
Karax is a framework for developing single page applications in Nim.
|
||||
It's still in heavy development, so keep in mind that the API is subject
|
||||
to change.
|
||||
|
||||
To try it out, run::
|
||||
|
||||
cd examples/todoapp
|
||||
nim js todoapp.nim
|
||||
open todoapp.html
|
||||
|
||||
|
||||
It uses a virtual DOM like React, but is much smaller than the existing
|
||||
frameworks plus of course it's written in Nim for Nim. No external
|
||||
dependencies! And thanks to Nim's whole program optimization only what
|
||||
is used ends up producing JavaScript code.
|
||||
|
||||
|
||||
Goals
|
||||
=====
|
||||
|
||||
- Leverage Nim's macro system to produce a framework that allows
|
||||
for the development of applications that are boilerplate free.
|
||||
- Keep it small, keep it fast, keep it flexible.
|
||||
564
src/dom.nim
564
src/dom.nim
|
|
@ -1,564 +0,0 @@
|
|||
#
|
||||
#
|
||||
# Nim's Runtime Library
|
||||
# (c) Copyright 2012 Andreas Rumpf
|
||||
#
|
||||
# See the file "copying.txt", included in this
|
||||
# distribution, for details about the copyright.
|
||||
#
|
||||
|
||||
## Declaration of the Document Object Model for the `JavaScript backend
|
||||
## <backends.html#the-javascript-target>`_.
|
||||
|
||||
when not defined(js) and not defined(Nimdoc):
|
||||
{.error: "This module only works on the JavaScript platform".}
|
||||
|
||||
type
|
||||
EventTarget* = ref EventTargetObj
|
||||
EventTargetObj {.importc.} = object of RootObj
|
||||
onabort*: proc (event: Event) {.nimcall.}
|
||||
onblur*: proc (event: Event) {.nimcall.}
|
||||
onchange*: proc (event: Event) {.nimcall.}
|
||||
onclick*: proc (event: Event) {.nimcall.}
|
||||
ondblclick*: proc (event: Event) {.nimcall.}
|
||||
onerror*: proc (event: Event) {.nimcall.}
|
||||
onfocus*: proc (event: Event) {.nimcall.}
|
||||
onkeydown*: proc (event: Event) {.nimcall.}
|
||||
onkeypress*: proc (event: Event) {.nimcall.}
|
||||
onkeyup*: proc (event: Event) {.nimcall.}
|
||||
onload*: proc (event: Event) {.nimcall.}
|
||||
onmousedown*: proc (event: Event) {.nimcall.}
|
||||
onmousemove*: proc (event: Event) {.nimcall.}
|
||||
onmouseout*: proc (event: Event) {.nimcall.}
|
||||
onmouseover*: proc (event: Event) {.nimcall.}
|
||||
onmouseup*: proc (event: Event) {.nimcall.}
|
||||
onreset*: proc (event: Event) {.nimcall.}
|
||||
onselect*: proc (event: Event) {.nimcall.}
|
||||
onsubmit*: proc (event: Event) {.nimcall.}
|
||||
onunload*: proc (event: Event) {.nimcall.}
|
||||
|
||||
Window* = ref WindowObj
|
||||
WindowObj {.importc.} = object of EventTargetObj
|
||||
document*: Document
|
||||
event*: Event
|
||||
history*: History
|
||||
location*: Location
|
||||
closed*: bool
|
||||
defaultStatus*: cstring
|
||||
innerHeight*, innerWidth*: int
|
||||
locationbar*: ref TLocationBar
|
||||
menubar*: ref TMenuBar
|
||||
name*: cstring
|
||||
outerHeight*, outerWidth*: int
|
||||
pageXOffset*, pageYOffset*: int
|
||||
personalbar*: ref TPersonalBar
|
||||
scrollbars*: ref TScrollBars
|
||||
statusbar*: ref TStatusBar
|
||||
status*: cstring
|
||||
toolbar*: ref TToolBar
|
||||
frames*: seq[TFrame]
|
||||
|
||||
Frame* = ref FrameObj
|
||||
FrameObj {.importc.} = object of WindowObj
|
||||
|
||||
ClassList* = ref ClassListObj
|
||||
ClassListObj {.importc.} = object of RootObj
|
||||
|
||||
NodeType* = enum
|
||||
ElementNode = 1,
|
||||
AttributeNode,
|
||||
TextNode,
|
||||
CDATANode,
|
||||
EntityRefNode,
|
||||
EntityNode,
|
||||
ProcessingInstructionNode,
|
||||
CommentNode,
|
||||
DocumentNode,
|
||||
DocumentTypeNode,
|
||||
DocumentFragmentNode,
|
||||
NotationNode
|
||||
|
||||
Node* = ref NodeObj
|
||||
NodeObj {.importc.} = object of EventTargetObj
|
||||
attributes*: seq[Node]
|
||||
childNodes*: seq[Node]
|
||||
children*: seq[Node]
|
||||
data*: cstring
|
||||
firstChild*: Node
|
||||
lastChild*: Node
|
||||
nextSibling*: Node
|
||||
nodeName*: cstring
|
||||
nodeType*: NodeType
|
||||
nodeValue*: cstring
|
||||
parentNode*: Node
|
||||
previousSibling*: Node
|
||||
innerHTML*: cstring
|
||||
style*: Style
|
||||
|
||||
Document* = ref DocumentObj
|
||||
DocumentObj {.importc.} = object of NodeObj
|
||||
alinkColor*: cstring
|
||||
bgColor*: cstring
|
||||
body*: Element
|
||||
charset*: cstring
|
||||
cookie*: cstring
|
||||
defaultCharset*: cstring
|
||||
fgColor*: cstring
|
||||
head*: Element
|
||||
lastModified*: cstring
|
||||
linkColor*: cstring
|
||||
referrer*: cstring
|
||||
title*: cstring
|
||||
URL*: cstring
|
||||
vlinkColor*: cstring
|
||||
anchors*: seq[AnchorElement]
|
||||
forms*: seq[FormElement]
|
||||
images*: seq[ImageElement]
|
||||
applets*: seq[Element]
|
||||
embeds*: seq[EmbedElement]
|
||||
links*: seq[LinkElement]
|
||||
|
||||
Element* = ref ElementObj
|
||||
ElementObj {.importc.} = object of NodeObj
|
||||
classList*: Classlist
|
||||
checked*: bool
|
||||
defaultChecked*: bool
|
||||
defaultValue*: cstring
|
||||
disabled*: bool
|
||||
form*: FormElement
|
||||
name*: cstring
|
||||
readOnly*: bool
|
||||
options*: seq[OptionElement]
|
||||
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
|
||||
HtmlElement* = ref object of Element
|
||||
contentEditable*: string
|
||||
isContentEditable*: bool
|
||||
dir*: string
|
||||
offsetHeight*: int
|
||||
offsetWidth*: int
|
||||
offsetLeft*: int
|
||||
offsetTop*: int
|
||||
|
||||
LinkElement* = ref LinkObj
|
||||
LinkObj {.importc.} = object of ElementObj
|
||||
target*: cstring
|
||||
text*: cstring
|
||||
x*: int
|
||||
y*: int
|
||||
|
||||
EmbedElement* = ref EmbedObj
|
||||
EmbedObj {.importc.} = object of ElementObj
|
||||
height*: int
|
||||
hspace*: int
|
||||
src*: cstring
|
||||
width*: int
|
||||
`type`*: cstring
|
||||
vspace*: int
|
||||
|
||||
AnchorElement* = ref AnchorObj
|
||||
AnchorObj {.importc.} = object of ElementObj
|
||||
text*: cstring
|
||||
x*, y*: int
|
||||
|
||||
OptionElement* = ref OptionObj
|
||||
OptionObj {.importc.} = object of ElementObj
|
||||
defaultSelected*: bool
|
||||
selected*: bool
|
||||
selectedIndex*: int
|
||||
text*: cstring
|
||||
value*: cstring
|
||||
|
||||
FormElement* = ref FormObj
|
||||
FormObj {.importc.} = object of ElementObj
|
||||
action*: cstring
|
||||
encoding*: cstring
|
||||
`method`*: cstring
|
||||
target*: cstring
|
||||
elements*: seq[Element]
|
||||
|
||||
ImageElement* = ref ImageObj
|
||||
ImageObj {.importc.} = object of ElementObj
|
||||
border*: int
|
||||
complete*: bool
|
||||
height*: int
|
||||
hspace*: int
|
||||
lowsrc*: cstring
|
||||
src*: cstring
|
||||
vspace*: int
|
||||
width*: int
|
||||
|
||||
Style = ref StyleObj
|
||||
StyleObj {.importc.} = object of RootObj
|
||||
background*: cstring
|
||||
backgroundAttachment*: cstring
|
||||
backgroundColor*: cstring
|
||||
backgroundImage*: cstring
|
||||
backgroundPosition*: cstring
|
||||
backgroundRepeat*: cstring
|
||||
border*: cstring
|
||||
borderBottom*: cstring
|
||||
borderBottomColor*: cstring
|
||||
borderBottomStyle*: cstring
|
||||
borderBottomWidth*: cstring
|
||||
borderColor*: cstring
|
||||
borderLeft*: cstring
|
||||
borderLeftColor*: cstring
|
||||
borderLeftStyle*: cstring
|
||||
borderLeftWidth*: cstring
|
||||
borderRight*: cstring
|
||||
borderRightColor*: cstring
|
||||
borderRightStyle*: cstring
|
||||
borderRightWidth*: cstring
|
||||
borderStyle*: cstring
|
||||
borderTop*: cstring
|
||||
borderTopColor*: cstring
|
||||
borderTopStyle*: cstring
|
||||
borderTopWidth*: cstring
|
||||
borderWidth*: cstring
|
||||
bottom*: cstring
|
||||
captionSide*: cstring
|
||||
clear*: cstring
|
||||
clip*: cstring
|
||||
color*: cstring
|
||||
cursor*: cstring
|
||||
direction*: cstring
|
||||
display*: cstring
|
||||
emptyCells*: cstring
|
||||
cssFloat*: cstring
|
||||
font*: cstring
|
||||
fontFamily*: cstring
|
||||
fontSize*: cstring
|
||||
fontStretch*: cstring
|
||||
fontStyle*: cstring
|
||||
fontVariant*: cstring
|
||||
fontWeight*: cstring
|
||||
height*: cstring
|
||||
left*: cstring
|
||||
letterSpacing*: cstring
|
||||
lineHeight*: cstring
|
||||
listStyle*: cstring
|
||||
listStyleImage*: cstring
|
||||
listStylePosition*: cstring
|
||||
listStyleType*: cstring
|
||||
margin*: cstring
|
||||
marginBottom*: cstring
|
||||
marginLeft*: cstring
|
||||
marginRight*: cstring
|
||||
marginTop*: cstring
|
||||
maxHeight*: cstring
|
||||
maxWidth*: cstring
|
||||
minHeight*: cstring
|
||||
minWidth*: cstring
|
||||
overflow*: cstring
|
||||
padding*: cstring
|
||||
paddingBottom*: cstring
|
||||
paddingLeft*: cstring
|
||||
paddingRight*: cstring
|
||||
paddingTop*: cstring
|
||||
pageBreakAfter*: cstring
|
||||
pageBreakBefore*: cstring
|
||||
position*: cstring
|
||||
right*: cstring
|
||||
scrollbar3dLightColor*: cstring
|
||||
scrollbarArrowColor*: cstring
|
||||
scrollbarBaseColor*: cstring
|
||||
scrollbarDarkshadowColor*: cstring
|
||||
scrollbarFaceColor*: cstring
|
||||
scrollbarHighlightColor*: cstring
|
||||
scrollbarShadowColor*: cstring
|
||||
scrollbarTrackColor*: cstring
|
||||
tableLayout*: cstring
|
||||
textAlign*: cstring
|
||||
textDecoration*: cstring
|
||||
textIndent*: cstring
|
||||
textTransform*: cstring
|
||||
top*: cstring
|
||||
verticalAlign*: cstring
|
||||
visibility*: cstring
|
||||
width*: cstring
|
||||
wordSpacing*: cstring
|
||||
zIndex*: int
|
||||
|
||||
# TODO: A lot of the fields in Event belong to a more specific type of event.
|
||||
# TODO: Should we clean this up?
|
||||
Event* = ref EventObj
|
||||
EventObj {.importc.} = object of RootObj
|
||||
target*: Node
|
||||
altKey*, ctrlKey*, shiftKey*: bool
|
||||
button*: int
|
||||
clientX*, clientY*: int
|
||||
keyCode*: int
|
||||
layerX*, layerY*: int
|
||||
modifiers*: int
|
||||
ALT_MASK*, CONTROL_MASK*, SHIFT_MASK*, META_MASK*: int
|
||||
offsetX*, offsetY*: int
|
||||
pageX*, pageY*: int
|
||||
screenX*, screenY*: int
|
||||
which*: int
|
||||
`type`*: cstring
|
||||
x*, y*: int
|
||||
ABORT*: int
|
||||
BLUR*: int
|
||||
CHANGE*: int
|
||||
CLICK*: int
|
||||
DBLCLICK*: int
|
||||
DRAGDROP*: int
|
||||
ERROR*: int
|
||||
FOCUS*: int
|
||||
KEYDOWN*: int
|
||||
KEYPRESS*: int
|
||||
KEYUP*: int
|
||||
LOAD*: int
|
||||
MOUSEDOWN*: int
|
||||
MOUSEMOVE*: int
|
||||
MOUSEOUT*: int
|
||||
MOUSEOVER*: int
|
||||
MOUSEUP*: int
|
||||
MOVE*: int
|
||||
RESET*: int
|
||||
RESIZE*: int
|
||||
SELECT*: int
|
||||
SUBMIT*: int
|
||||
UNLOAD*: int
|
||||
|
||||
TouchList* {.importc.} = ref object of RootObj
|
||||
length*: int
|
||||
|
||||
TouchEvent* {.importc.} = ref object of Event
|
||||
changedTouches*, targetTouches*, touches*: TouchList
|
||||
|
||||
Touch* {.importc.} = ref object of RootObj
|
||||
identifier*: int
|
||||
screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int
|
||||
target*: Element
|
||||
radiusX*, radiusY*: int
|
||||
rotationAngle*: int
|
||||
force*: float
|
||||
|
||||
Location* = ref LocationObj
|
||||
LocationObj {.importc.} = object of RootObj
|
||||
hash*: cstring
|
||||
host*: cstring
|
||||
hostname*: cstring
|
||||
href*: cstring
|
||||
pathname*: cstring
|
||||
port*: cstring
|
||||
protocol*: cstring
|
||||
search*: cstring
|
||||
|
||||
History* = ref HistoryObj
|
||||
HistoryObj {.importc.} = object of RootObj
|
||||
length*: int
|
||||
|
||||
Navigator* = ref NavigatorObj
|
||||
NavigatorObj {.importc.} = object of RootObj
|
||||
appCodeName*: cstring
|
||||
appName*: cstring
|
||||
appVersion*: cstring
|
||||
cookieEnabled*: bool
|
||||
language*: cstring
|
||||
platform*: cstring
|
||||
userAgent*: cstring
|
||||
mimeTypes*: seq[ref TMimeType]
|
||||
|
||||
TPlugin* {.importc.} = object of RootObj
|
||||
description*: cstring
|
||||
filename*: cstring
|
||||
name*: cstring
|
||||
|
||||
TMimeType* {.importc.} = object of RootObj
|
||||
description*: cstring
|
||||
enabledPlugin*: ref TPlugin
|
||||
suffixes*: seq[cstring]
|
||||
`type`*: cstring
|
||||
|
||||
TLocationBar* {.importc.} = object of RootObj
|
||||
visible*: bool
|
||||
TMenuBar* = TLocationBar
|
||||
TPersonalBar* = TLocationBar
|
||||
TScrollBars* = TLocationBar
|
||||
TToolBar* = TLocationBar
|
||||
TStatusBar* = TLocationBar
|
||||
|
||||
Screen = ref ScreenObj
|
||||
ScreenObj {.importc.} = object of RootObj
|
||||
availHeight*: int
|
||||
availWidth*: int
|
||||
colorDepth*: int
|
||||
height*: int
|
||||
pixelDepth*: int
|
||||
width*: int
|
||||
|
||||
TTimeOut* {.importc.} = object of RootObj
|
||||
TInterval* {.importc.} = object of RootObj
|
||||
|
||||
{.push importcpp.}
|
||||
|
||||
# EventTarget "methods"
|
||||
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), useCapture: bool = false)
|
||||
|
||||
# Window "methods"
|
||||
proc alert*(w: Window, msg: cstring)
|
||||
proc back*(w: Window)
|
||||
proc blur*(w: Window)
|
||||
proc captureEvents*(w: Window, eventMask: int) {.deprecated.}
|
||||
proc clearInterval*(w: Window, interval: ref TInterval)
|
||||
proc clearTimeout*(w: Window, timeout: ref TTimeOut)
|
||||
proc close*(w: Window)
|
||||
proc confirm*(w: Window, msg: cstring): bool
|
||||
proc disableExternalCapture*(w: Window)
|
||||
proc enableExternalCapture*(w: Window)
|
||||
proc find*(w: Window, text: cstring, caseSensitive = false,
|
||||
backwards = false)
|
||||
proc focus*(w: Window)
|
||||
proc forward*(w: Window)
|
||||
proc handleEvent*(w: Window, e: Event)
|
||||
proc home*(w: Window)
|
||||
proc moveBy*(w: Window, x, y: int)
|
||||
proc moveTo*(w: Window, x, y: int)
|
||||
proc open*(w: Window, uri, windowname: cstring,
|
||||
properties: cstring = nil): Window
|
||||
proc print*(w: Window)
|
||||
proc prompt*(w: Window, text, default: cstring): cstring
|
||||
proc releaseEvents*(w: Window, eventMask: int) {.deprecated.}
|
||||
proc resizeBy*(w: Window, x, y: int)
|
||||
proc resizeTo*(w: Window, x, y: int)
|
||||
proc routeEvent*(w: Window, event: Event)
|
||||
proc scrollBy*(w: Window, x, y: int)
|
||||
proc scrollTo*(w: Window, x, y: int)
|
||||
proc setInterval*(w: Window, code: cstring, pause: int): ref TInterval
|
||||
proc setInterval*(w: Window, function: proc (), pause: int): ref TInterval
|
||||
proc setTimeout*(w: Window, code: cstring, pause: int): ref TTimeOut
|
||||
proc setTimeout*(w: Window, function: proc (), pause: int): ref TInterval
|
||||
proc stop*(w: Window)
|
||||
proc requestAnimationFrame*(w: Window, function: proc (time: float)): int
|
||||
proc cancelAnimationFrame*(w: Window, id: int)
|
||||
|
||||
# Node "methods"
|
||||
proc appendChild*(n, child: Node)
|
||||
proc appendData*(n: Node, data: cstring)
|
||||
proc cloneNode*(n: Node, copyContent: bool): Node
|
||||
proc deleteData*(n: Node, start, len: int)
|
||||
proc getAttribute*(n: Node, attr: cstring): cstring
|
||||
proc getAttributeNode*(n: Node, attr: cstring): Node
|
||||
proc hasChildNodes*(n: Node): bool
|
||||
proc insertBefore*(n, newNode, before: Node)
|
||||
proc insertData*(n: Node, position: int, data: cstring)
|
||||
proc removeAttribute*(n: Node, attr: cstring)
|
||||
proc removeAttributeNode*(n, attr: Node)
|
||||
proc removeChild*(n, child: Node)
|
||||
proc replaceChild*(n, newNode, oldNode: Node)
|
||||
proc replaceData*(n: Node, start, len: int, text: cstring)
|
||||
proc scrollIntoView*(n: Node)
|
||||
proc setAttribute*(n: Node, name, value: cstring)
|
||||
proc setAttributeNode*(n: Node, attr: Node)
|
||||
|
||||
# Document "methods"
|
||||
proc captureEvents*(d: Document, eventMask: int) {.deprecated.}
|
||||
proc createAttribute*(d: Document, identifier: cstring): Node
|
||||
proc createElement*(d: Document, identifier: cstring): Element
|
||||
proc createTextNode*(d: Document, identifier: cstring): Node
|
||||
proc getElementById*(d: Document, id: cstring): Element
|
||||
proc getElementsByName*(d: Document, name: cstring): seq[Element]
|
||||
proc getElementsByTagName*(d: Document, name: cstring): seq[Element]
|
||||
proc getElementsByClassName*(d: Document, name: cstring): seq[Element]
|
||||
proc getSelection*(d: Document): cstring
|
||||
proc handleEvent*(d: Document, event: Event)
|
||||
proc open*(d: Document)
|
||||
proc releaseEvents*(d: Document, eventMask: int) {.deprecated.}
|
||||
proc routeEvent*(d: Document, event: Event)
|
||||
proc write*(d: Document, text: cstring)
|
||||
proc writeln*(d: Document, text: cstring)
|
||||
|
||||
# Element "methods"
|
||||
proc blur*(e: Element)
|
||||
proc click*(e: Element)
|
||||
proc focus*(e: Node)
|
||||
proc handleEvent*(e: Element, event: Event)
|
||||
proc select*(e: Element)
|
||||
proc getElementsByTagName*(e: Element, name: cstring): seq[Element]
|
||||
proc getElementsByClassName*(e: Element, name: cstring): seq[Element]
|
||||
|
||||
# FormElement "methods"
|
||||
proc reset*(f: FormElement)
|
||||
proc submit*(f: FormElement)
|
||||
|
||||
# EmbedElement "methods"
|
||||
proc play*(e: EmbedElement)
|
||||
proc stop*(e: EmbedElement)
|
||||
|
||||
# Location "methods"
|
||||
proc reload*(loc: Location)
|
||||
proc replace*(loc: Location, s: cstring)
|
||||
|
||||
# History "methods"
|
||||
proc back*(h: History)
|
||||
proc forward*(h: History)
|
||||
proc go*(h: History, pagesToJump: int)
|
||||
|
||||
# Navigator "methods"
|
||||
proc javaEnabled*(h: Navigator): bool
|
||||
|
||||
# ClassList "methods"
|
||||
proc add*(c: ClassList, class: cstring)
|
||||
proc remove*(c: ClassList, class: cstring)
|
||||
proc contains*(c: ClassList, class: cstring):bool
|
||||
proc toggle*(c: ClassList, class: cstring)
|
||||
|
||||
# Style "methods"
|
||||
proc getAttribute*(s: Style, attr: cstring, caseSensitive=false): cstring
|
||||
proc removeAttribute*(s: Style, attr: cstring, caseSensitive=false)
|
||||
proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false)
|
||||
|
||||
# Event "methods"
|
||||
proc preventDefault*(ev: Event)
|
||||
|
||||
# TouchEvent "methods"
|
||||
proc identifiedTouch*(list: TouchList): Touch
|
||||
proc item*(list: TouchList, i: int): Touch
|
||||
|
||||
{.pop.}
|
||||
|
||||
var
|
||||
window* {.importc, nodecl.}: Window
|
||||
document* {.importc, nodecl.}: Document
|
||||
navigator* {.importc, nodecl.}: Navigator
|
||||
screen* {.importc, nodecl.}: Screen
|
||||
|
||||
proc decodeURI*(uri: cstring): cstring {.importc, nodecl.}
|
||||
proc encodeURI*(uri: cstring): cstring {.importc, nodecl.}
|
||||
|
||||
proc escape*(uri: cstring): cstring {.importc, nodecl.}
|
||||
proc unescape*(uri: cstring): cstring {.importc, nodecl.}
|
||||
|
||||
proc decodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
|
||||
proc encodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
|
||||
proc isFinite*(x: BiggestFloat): bool {.importc, nodecl.}
|
||||
proc isNaN*(x: BiggestFloat): bool {.importc, nodecl.}
|
||||
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}
|
||||
proc parseInt*(s: cstring): int {.importc, nodecl.}
|
||||
proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.}
|
||||
|
||||
|
||||
type
|
||||
TEventHandlers* {.deprecated.} = EventTargetObj
|
||||
TWindow* {.deprecated.} = WindowObj
|
||||
TFrame* {.deprecated.} = FrameObj
|
||||
TNode* {.deprecated.} = NodeObj
|
||||
TDocument* {.deprecated.} = DocumentObj
|
||||
TElement* {.deprecated.} = ElementObj
|
||||
TLink* {.deprecated.} = LinkObj
|
||||
TEmbed* {.deprecated.} = EmbedObj
|
||||
TAnchor* {.deprecated.} = AnchorObj
|
||||
TOption* {.deprecated.} = OptionObj
|
||||
TForm* {.deprecated.} = FormObj
|
||||
TImage* {.deprecated.} = ImageObj
|
||||
TNodeType* {.deprecated.} = NodeType
|
||||
TEvent* {.deprecated.} = EventObj
|
||||
TLocation* {.deprecated.} = LocationObj
|
||||
THistory* {.deprecated.} = HistoryObj
|
||||
TNavigator* {.deprecated.} = NavigatorObj
|
||||
TStyle* {.deprecated.} = StyleObj
|
||||
TScreen* {.deprecated.} = ScreenObj
|
||||
TApplet* {.importc, deprecated.} = object of RootObj
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
|
||||
type
|
||||
JDict*[K, V] = ref object
|
||||
|
||||
proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".}
|
||||
proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".}
|
||||
|
||||
proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
|
||||
|
||||
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
|
||||
|
||||
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
|
||||
|
||||
523
src/karax.nim
523
src/karax.nim
|
|
@ -1,523 +0,0 @@
|
|||
# Simple lib to write JS UIs
|
||||
|
||||
import dom, vdom, jstrutils
|
||||
|
||||
export dom.Event, dom.cloneNode, dom
|
||||
|
||||
proc len(x: Node): int {.importcpp: "#.childNodes.length".}
|
||||
proc `[]`(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".}
|
||||
|
||||
proc kout*[T](x: T) {.importc: "console.log", varargs.}
|
||||
## the preferred way of debugging karax applications.
|
||||
|
||||
proc id*(e: Node): cstring {.importcpp: "#.id", nodecl.}
|
||||
proc `id=`*(e: Node; x: cstring) {.importcpp: "#.id = #", nodecl.}
|
||||
proc class*(e: Node): cstring {.importcpp: "#.className", nodecl.}
|
||||
proc `class=`*(e: Node; v: cstring) {.importcpp: "#.className = #", nodecl.}
|
||||
|
||||
proc value*(e: Node): cstring {.importcpp: "#.value", nodecl.}
|
||||
proc `value=`*(e: Node; v: cstring) {.importcpp: "#.value = #", nodecl.}
|
||||
|
||||
proc `disabled=`*(e: Node; v: bool) {.importcpp: "#.disabled = #", nodecl.}
|
||||
|
||||
proc getElementsByClass*(e: Node; name: cstring): seq[Node] {.
|
||||
importcpp: "#.getElementsByClassName(#)", nodecl.}
|
||||
|
||||
proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".}
|
||||
proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.}
|
||||
proc key*(e: Node): VKey =
|
||||
if e.hasProp"karaxKey": result = e.rawkey
|
||||
else: result = -1
|
||||
proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.}
|
||||
|
||||
type
|
||||
BoundingRect {.importc.} = object
|
||||
top, bottom, left, right: int
|
||||
|
||||
proc getBoundingClientRect(e: Node): BoundingRect {.
|
||||
importcpp: "getBoundingClientRect", nodecl.}
|
||||
proc clientHeight(): int {.
|
||||
importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl}
|
||||
proc clientWidth(): int {.
|
||||
importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl}
|
||||
|
||||
when false:
|
||||
proc pageYOffset(): int {.
|
||||
importcpp: "(window.pageYOffset)@", nodecl}
|
||||
proc pageXOffset(): int {.
|
||||
importcpp: "(window.pageXOffset)@", nodecl}
|
||||
|
||||
type
|
||||
Timeout* = ref object
|
||||
|
||||
var
|
||||
document* {.importc.}: Document
|
||||
toFocus: Node
|
||||
toFocusV: VNode
|
||||
|
||||
proc setFocus*(n: VNode) =
|
||||
toFocusV = n
|
||||
|
||||
proc isElementInViewport(el: Node; h: var int): bool =
|
||||
let rect = el.getBoundingClientRect()
|
||||
h = rect.bottom - rect.top
|
||||
result = rect.top >= 0 and rect.left >= 0 and
|
||||
rect.bottom <= clientHeight() and
|
||||
rect.right <= clientWidth()
|
||||
|
||||
proc vnodeToDom(n: VNode): Node =
|
||||
if n.kind == VNodeKind.text:
|
||||
result = document.createTextNode(n.text)
|
||||
elif n.kind == VNodeKind.thunk:
|
||||
return vnodeToDom(n.callThunk)
|
||||
else:
|
||||
result = document.createElement(toTag[n.kind])
|
||||
for k in n:
|
||||
appendChild(result, vnodeToDom(k))
|
||||
# text is mapped to 'value':
|
||||
if n.text != nil:
|
||||
result.value = n.text
|
||||
if n.id != nil:
|
||||
result.id = n.id
|
||||
if n.class != nil:
|
||||
result.class = n.class
|
||||
if n.key >= 0:
|
||||
result.key = n.key
|
||||
for k, v in attrs(n):
|
||||
if v != nil:
|
||||
result.setAttribute(k, v)
|
||||
let myn = n
|
||||
for e, h in items(n.events):
|
||||
proc wrapper(): proc (ev: Event) =
|
||||
let hh = h
|
||||
result = proc (ev: Event) =
|
||||
assert myn != nil
|
||||
hh(ev, myn)
|
||||
result.addEventListener(toEventName[e], wrapper())
|
||||
if n == toFocusV and toFocus.isNil:
|
||||
toFocus = result
|
||||
|
||||
proc same(n: VNode, e: Node): bool =
|
||||
if toTag[n.kind] == e.nodename:
|
||||
result = true
|
||||
if n.kind != VNodeKind.text:
|
||||
if e.len != n.len: return false
|
||||
for i in 0 ..< n.len:
|
||||
if not same(n[i], e[i]): return false
|
||||
|
||||
var
|
||||
dorender: proc (): VNode {.closure.}
|
||||
drawTimeout: Timeout
|
||||
currentTree: VNode
|
||||
|
||||
proc setRenderer*(renderer: proc (): VNode) =
|
||||
dorender = renderer
|
||||
|
||||
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
|
||||
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
|
||||
#proc targetElem*(e: Event): Element = cast[Element](e.target)
|
||||
|
||||
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
|
||||
|
||||
#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
|
||||
# "document.getElementsByClassName", nodecl.}
|
||||
|
||||
proc textContent(e: Node): cstring {.
|
||||
importcpp: "#.textContent", nodecl.}
|
||||
|
||||
proc replaceById(id: cstring; newTree: Node) =
|
||||
let x = document.getElementById(id)
|
||||
x.parentNode.replaceChild(newTree, x)
|
||||
#newTree.id = id
|
||||
|
||||
proc equals(a, b: VNode): bool =
|
||||
if a.kind != b.kind: return false
|
||||
if a.id != b.id: return false
|
||||
if a.key != b.key: return false
|
||||
if a.kind == VNodeKind.text:
|
||||
if a.text != b.text: return false
|
||||
elif a.kind == VNodeKind.thunk:
|
||||
if a.thunk != b.thunk: return false
|
||||
if a.len != b.len: return false
|
||||
for i in 0..<a.len:
|
||||
if not equals(a[i], b[i]): return false
|
||||
if not sameAttrs(a, b): return false
|
||||
if a.class != b.class: return false
|
||||
# XXX test event listeners here?
|
||||
# --> maybe give nodes a hash?
|
||||
return true
|
||||
|
||||
proc equalsTree(a, b: VNode): bool =
|
||||
if not a.validHash:
|
||||
a.calcHash()
|
||||
if not b.validHash:
|
||||
b.calcHash()
|
||||
return a.hash == b.hash
|
||||
|
||||
proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
|
||||
if not equals(newNode, oldNode):
|
||||
let n = vnodeToDom(newNode)
|
||||
if parent == nil:
|
||||
replaceById("ROOT", n)
|
||||
else:
|
||||
parent.replaceChild(n, current)
|
||||
elif newNode.kind != VNodeKind.text:
|
||||
let newLength = newNode.len
|
||||
var oldLength = oldNode.len
|
||||
let minLength = min(newLength, oldLength)
|
||||
assert oldNode.kind == newNode.kind
|
||||
when defined(simpleDiff):
|
||||
for i in 0..min(newLength, oldLength)-1:
|
||||
updateElement(current, current[i], newNode[i], oldNode[i])
|
||||
if newLength > oldLength:
|
||||
for i in oldLength..newLength-1:
|
||||
current.appendChild(vnodeToDom(newNode[i]))
|
||||
elif oldLength > newLength:
|
||||
for i in countdown(oldLength-1, newLength):
|
||||
current.removeChild(current.lastChild)
|
||||
else:
|
||||
var commonPrefix = 0
|
||||
while commonPrefix < minLength and equalsTree(newNode[commonPrefix], oldNode[commonPrefix]):
|
||||
inc commonPrefix
|
||||
|
||||
var oldPos = oldLength - 1
|
||||
var newPos = newLength - 1
|
||||
while oldPos >= commonPrefix and newPos >= commonPrefix and equalsTree(newNode[newPos], oldNode[oldPos]):
|
||||
dec oldPos
|
||||
dec newPos
|
||||
|
||||
var pos = min(oldPos, newPos) + 1
|
||||
for i in commonPrefix..pos-1:
|
||||
updateElement(current, current.childNodes[i],
|
||||
newNode[i],
|
||||
oldNode[i])
|
||||
|
||||
var nextChildPos = oldPos + 1
|
||||
while pos <= newPos:
|
||||
if nextChildPos == oldLength:
|
||||
current.appendChild(vnodeToDom(newNode[pos]))
|
||||
else:
|
||||
current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos])
|
||||
# added new Node, so old state of VDOM have one more Node
|
||||
inc oldLength
|
||||
inc pos
|
||||
inc nextChildPos
|
||||
|
||||
for i in 0..oldPos-pos:
|
||||
current.removeChild(current.childNodes[pos])
|
||||
|
||||
|
||||
proc dodraw() =
|
||||
let newtree = dorender()
|
||||
newtree.id = "ROOT"
|
||||
toFocus = nil
|
||||
if currentTree == nil:
|
||||
currentTree = newtree
|
||||
let asdom = vnodeToDom currentTree
|
||||
replaceById("ROOT", asdom)
|
||||
else:
|
||||
let olddom = document.getElementById("ROOT")
|
||||
updateElement(nil, olddom, newtree, currentTree)
|
||||
#assert same(newtree, document.getElementById("ROOT"))
|
||||
currentTree = newtree
|
||||
# now that it's part of the DOM, give it the focus:
|
||||
if toFocus != nil:
|
||||
toFocus.focus()
|
||||
|
||||
proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
|
||||
# we only care about nodes that have a key:
|
||||
var hh = 0
|
||||
# do not recurse if there is a 'key' field already:
|
||||
if e.key >= 0:
|
||||
if isElementInViewport(e, hh):
|
||||
inc count
|
||||
inc h, hh
|
||||
a = min(a, e.key)
|
||||
b = max(b, e.key)
|
||||
else:
|
||||
for i in 0..<e.len:
|
||||
visibleKeys(e[i], a, b, h, count)
|
||||
|
||||
proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".}
|
||||
|
||||
proc redraw*() =
|
||||
# we buffer redraw requests:
|
||||
when false:
|
||||
if drawTimeout != nil:
|
||||
clearTimeout(drawTimeout)
|
||||
drawTimeout = setTimeout(dodraw, 30)
|
||||
elif true:
|
||||
reqFrame(dodraw)
|
||||
else:
|
||||
dodraw()
|
||||
|
||||
proc init*() =
|
||||
reqFrame(dodraw)
|
||||
|
||||
#proc prepend*(parent, kid: Element) =
|
||||
# parent.insertBefore(kid, parent.firstChild)
|
||||
# prependChild(parent, kid)
|
||||
|
||||
proc suffix*(s, prefix: cstring): cstring =
|
||||
if s.startsWith(prefix):
|
||||
result = s.substr(prefix.len)
|
||||
else:
|
||||
kout(cstring"bug! " & s & cstring" does not start with " & prefix)
|
||||
|
||||
proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
|
||||
|
||||
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
|
||||
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
|
||||
proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
|
||||
|
||||
template onImpl(s) {.dirty.} =
|
||||
proc wrapper(ev: Event; n: VNode) =
|
||||
action(ev, n)
|
||||
redraw()
|
||||
addEventListener(e, s, wrapper)
|
||||
|
||||
proc setOnclick*(e: VNode; action: EventHandler) =
|
||||
onImpl EventKind.onclick
|
||||
|
||||
proc setOnDblclick*(e: VNode; action: EventHandler) =
|
||||
onImpl EventKind.ondblclick
|
||||
|
||||
proc setOnfocuslost*(e: VNode; action: EventHandler) =
|
||||
onImpl EventKind.onblur
|
||||
|
||||
proc setOnchanged*(e: VNode; action: EventHandler) =
|
||||
onImpl EventKind.onchange
|
||||
|
||||
proc setOnscroll*(e: VNode; action: EventHandler) =
|
||||
onImpl EventKind.onscroll
|
||||
|
||||
#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.}
|
||||
|
||||
proc setOnscroll*(action: proc(min, max: VKey; diff: int)) =
|
||||
var oldY = window.pageYOffset
|
||||
|
||||
proc wrapper(ev: Event) =
|
||||
let dir = window.pageYOffset - oldY
|
||||
if dir == 0: return
|
||||
|
||||
var a = VKey high(int)
|
||||
var b = VKey 0
|
||||
var h, count: int
|
||||
document.visibleKeys(a, b, h, count)
|
||||
let avgh = h / count
|
||||
let diff = toInt(dir.float / avgh)
|
||||
if diff != 0:
|
||||
oldY = window.pageYOffset
|
||||
action(a, b, diff)
|
||||
redraw()
|
||||
|
||||
document.addEventListener("scroll", wrapper)
|
||||
|
||||
proc setOnHashChange*(action: proc (hashPart: cstring)) =
|
||||
var onhashChange {.importc: "window.onhashchange".}: proc()
|
||||
var hashPart {.importc: "window.location.hash".}: cstring
|
||||
proc wrapper() =
|
||||
action(hashPart)
|
||||
redraw()
|
||||
onhashchange = wrapper
|
||||
|
||||
var
|
||||
linkCounter: int
|
||||
|
||||
proc link*(id: int): VNode =
|
||||
result = newVNode(VNodeKind.anchor)
|
||||
result.setAttr("href", "#")
|
||||
inc linkCounter
|
||||
result.setAttr("id", $linkCounter & ":" & $id)
|
||||
|
||||
proc link*(action: EventHandler): VNode =
|
||||
result = newVNode(VNodeKind.anchor)
|
||||
result.setAttr("href", "#")
|
||||
result.setOnclick action
|
||||
|
||||
when false:
|
||||
proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
|
||||
result = newVNode(VNodeKind.button)
|
||||
result.add text(caption)
|
||||
if action != nil:
|
||||
result.setOnClick action
|
||||
if disabled:
|
||||
result.setAttr("disabled", "true")
|
||||
|
||||
proc select*(choices: openarray[cstring]): VNode =
|
||||
result = newVNode(VNodeKind.select)
|
||||
var i = 0
|
||||
for c in choices:
|
||||
result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c))
|
||||
inc i
|
||||
|
||||
proc select*(choices: openarray[(int, cstring)]): VNode =
|
||||
result = newVNode(VNodeKind.select)
|
||||
for c in choices:
|
||||
result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1]))
|
||||
|
||||
var radioCounter: int
|
||||
|
||||
proc radio*(choices: openarray[(int, cstring)]): VNode =
|
||||
result = newVNode(VNodeKind.fieldset)
|
||||
var i = 0
|
||||
inc radioCounter
|
||||
for c in choices:
|
||||
let id = cstring"radio_" & c[1] & toCstr(i)
|
||||
var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"),
|
||||
(cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)),
|
||||
(cstring"value", toCStr(c[0]))])
|
||||
if i == 0:
|
||||
kid.setAttr(cstring"checked", cstring"checked")
|
||||
var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1]))
|
||||
kid.add lab
|
||||
result.add kid
|
||||
inc i
|
||||
|
||||
proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode =
|
||||
result = newVNode(kind)
|
||||
result.id = id
|
||||
result.class = class
|
||||
|
||||
proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class)
|
||||
proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class)
|
||||
|
||||
proc valueAsInt*(e: Node): int = parseInt(e.value)
|
||||
|
||||
proc th*(s: cstring): VNode =
|
||||
result = newVNode(VNodeKind.th)
|
||||
result.add text(s)
|
||||
|
||||
proc td*(s: string): VNode =
|
||||
result = newVNode(VNodeKind.td)
|
||||
result.add text(s)
|
||||
|
||||
proc td*(s: VNode): VNode =
|
||||
result = newVNode(VNodeKind.td)
|
||||
result.add s
|
||||
|
||||
proc td*(class: cstring; s: VNode): VNode =
|
||||
result = newVNode(VNodeKind.td)
|
||||
result.add s
|
||||
result.class = class
|
||||
|
||||
proc table*(class=cstring(nil), kids: varargs[VNode]): VNode =
|
||||
result = tag(VNodeKind.table, nil, class)
|
||||
for k in kids: result.add k
|
||||
|
||||
proc tr*(kids: varargs[VNode]): VNode =
|
||||
result = newVNode(VNodeKind.tr)
|
||||
for k in kids:
|
||||
if k.kind in {VNodeKind.td, VNodeKind.th}:
|
||||
result.add k
|
||||
else:
|
||||
result.add td(k)
|
||||
|
||||
proc getAttr(e: Node; key: cstring): cstring {.
|
||||
importcpp: "#.getAttribute(#)", nodecl.}
|
||||
|
||||
template nativeValue(ev): cstring = cast[Element](ev.target).value
|
||||
template setNativeValue(ev, val) = cast[Element](ev.target).value = val
|
||||
|
||||
template keyeventBody() =
|
||||
n.value = nativeValue(ev)
|
||||
action(ev, n)
|
||||
setNativeValue(ev, n.value)
|
||||
redraw()
|
||||
|
||||
proc realtimeInput*(val: cstring; action: EventHandler): VNode =
|
||||
#let oldElem = getElementById(id)
|
||||
#if oldElem != nil: return oldElem
|
||||
#let newVal = if oldElem.isNil: val else: $oldElem.value
|
||||
var timer: Timeout
|
||||
proc onkeyup(ev: Event; n: VNode) =
|
||||
proc wrapper() = keyeventBody()
|
||||
|
||||
if timer != nil: clearTimeout(timer)
|
||||
timer = setTimeout(wrapper, 400)
|
||||
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
|
||||
result.value = val
|
||||
result.addEventListener(EventKind.onkeyup, onkeyup)
|
||||
|
||||
proc enterInput*(id, val: cstring; action: EventHandler): VNode =
|
||||
#let oldElem = getElementById(id)
|
||||
#if oldElem != nil: return oldElem
|
||||
#let newVal = if oldElem.isNil: val else: $oldElem.value
|
||||
proc onkeyup(ev: Event; n: VNode) =
|
||||
if ev.keyCode == 13: keyeventBody()
|
||||
|
||||
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
|
||||
result.id = id
|
||||
result.value = val
|
||||
result.addEventListener(EventKind.onkeyup, onkeyup)
|
||||
|
||||
proc setOnEnter*(n: VNode; action: EventHandler) =
|
||||
proc onkeyup(ev: Event; n: VNode) =
|
||||
if ev.keyCode == 13: keyeventBody()
|
||||
n.addEventListener(EventKind.onkeyup, onkeyup)
|
||||
|
||||
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
|
||||
data: cstring;
|
||||
cont: proc (httpStatus: int; response: cstring)) =
|
||||
proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".}
|
||||
{.emit: """
|
||||
var ajax = new XMLHttpRequest();
|
||||
ajax.open(`meth`,`url`,true);""".}
|
||||
for a, b in items(headers):
|
||||
setRequestHeader(a, b)
|
||||
{.emit: """
|
||||
ajax.onreadystatechange = function(){
|
||||
if(this.readyState == 4){
|
||||
if(this.status == 200){
|
||||
`cont`(this.status, this.responseText);
|
||||
} else {
|
||||
`cont`(this.status, this.statusText);
|
||||
}
|
||||
}
|
||||
}
|
||||
ajax.send(`data`);
|
||||
""".}
|
||||
|
||||
proc ajaxPut*(url: string; headers: openarray[(cstring, cstring)];
|
||||
data: cstring;
|
||||
cont: proc (httpStatus: int, response: cstring)) =
|
||||
ajax("PUT", url, headers, data, cont)
|
||||
|
||||
proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)];
|
||||
cont: proc (httpStatus: int, response: cstring)) =
|
||||
ajax("GET", url, headers, nil, cont)
|
||||
|
||||
{.push stackTrace:off.}
|
||||
|
||||
proc setupErrorHandler*(useAlert=false) =
|
||||
## Installs an error handler that transforms native JS unhandled
|
||||
## exceptions into Nim based stack traces. If `useAlert` is false,
|
||||
## the error message is put into the console, otherwise `alert`
|
||||
## is called.
|
||||
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
|
||||
{.emit: """
|
||||
window.onerror = function(msg, url, line, col, error) {
|
||||
var x = "Error: " + msg + "\n" + `stackTraceAsCstring`()
|
||||
if (`useAlert`)
|
||||
alert(x);
|
||||
else
|
||||
console.log(x);
|
||||
var suppressErrorAlert = true;
|
||||
return suppressErrorAlert;
|
||||
};""".}
|
||||
|
||||
{.pop.}
|
||||
|
||||
when false:
|
||||
var plugins {.exportc.}: seq[(string, proc())] = @[]
|
||||
|
||||
proc onInput(val: cstring) =
|
||||
kout val
|
||||
if val == "dyn":
|
||||
let body = getElementById("body")
|
||||
body.prepend(tree("script", [("type", "text/javascript"), ("src", "nimcache/dyn.js")]))
|
||||
redraw()
|
||||
kout(plugins.len)
|
||||
if plugins.len > 0:
|
||||
plugins[0][1]()
|
||||
|
||||
113
src/karaxdsl.nim
113
src/karaxdsl.nim
|
|
@ -1,113 +0,0 @@
|
|||
|
||||
import macros, karax, vdom
|
||||
from strutils import startsWith, toLowerAscii
|
||||
|
||||
const
|
||||
StmtContext = ["kout", "inc", "echo", "dec", "!"]
|
||||
SpecialAttrs = ["id", "class", "value", "key"]
|
||||
|
||||
proc getName(n: NimNode): string =
|
||||
case n.kind
|
||||
of nnkIdent:
|
||||
result = $n.ident
|
||||
of nnkAccQuoted:
|
||||
result = ""
|
||||
for i in 0..<n.len:
|
||||
result.add getName(n[i])
|
||||
of nnkStrLit..nnkTripleStrLit:
|
||||
result = n.strVal
|
||||
else:
|
||||
#echo repr n
|
||||
expectKind(n, nnkIdent)
|
||||
|
||||
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
|
||||
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
|
||||
|
||||
proc isTag(s: string): bool =
|
||||
for i in VNodeKind.low.succ..VNodeKind.high:
|
||||
if $i == s: return true
|
||||
|
||||
proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||
# we need to distinguish statement and expression contexts:
|
||||
# every call statement 's' needs to be transformed to 'dest.add s'.
|
||||
# If expressions need to be distinguished from if statements. Since
|
||||
# we know we start in a statement context, it's pretty simple to
|
||||
# figure out expression contexts: In calls everything is an expression
|
||||
# (except for the last child of the macros we consider here),
|
||||
# lets, consts, types can be considered as expressions
|
||||
# case is complex, calls are assumed to produce a value.
|
||||
case n.kind
|
||||
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
|
||||
if tmpContext != nil:
|
||||
result = newCall(bindSym"add", tmpContext, n)
|
||||
else:
|
||||
result = n
|
||||
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
|
||||
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
|
||||
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
|
||||
# recurse for the last son:
|
||||
result = copyNimTree(n)
|
||||
let L = n.len
|
||||
assert n.len == result.len
|
||||
if L > 0:
|
||||
result[L-1] = tcall2(result[L-1], tmpContext)
|
||||
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt,
|
||||
nnkTryStmt, nnkFinally:
|
||||
# recurse for every child:
|
||||
result = copyNimNode(n)
|
||||
for x in n:
|
||||
result.add tcall2(x, tmpContext)
|
||||
of nnkVarSection, nnkLetSection, nnkConstSection:
|
||||
result = n
|
||||
of nnkCallKinds:
|
||||
let op = getName(n[0])
|
||||
if isTag(op):
|
||||
let tmp = genSym(nskLet, "tmp")
|
||||
result = newTree(
|
||||
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
|
||||
newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))))
|
||||
for i in 1 ..< n.len:
|
||||
# named parameters are transformed into attributes or events:
|
||||
let x = n[i]
|
||||
if x.kind == nnkExprEqExpr:
|
||||
let key = getName x[0]
|
||||
if key.startsWith("on"):
|
||||
result.add newCall(!("set" & key), tmp, x[1])
|
||||
elif key in SpecialAttrs:
|
||||
result.add newDotAsgn(tmp, key, x[1])
|
||||
else:
|
||||
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
|
||||
elif x.kind == nnkIdent:
|
||||
result.add newCall(x, tmp)
|
||||
else:
|
||||
result.add tcall2(x, tmp)
|
||||
if tmpContext == nil:
|
||||
result.add tmp
|
||||
else:
|
||||
result.add newCall(bindSym"add", tmpContext, tmp)
|
||||
elif tmpContext != nil and op notin StmtContext:
|
||||
result = newCall(bindSym"add", tmpContext, n)
|
||||
elif op == "!" and n.len == 2:
|
||||
result = n[1]
|
||||
else:
|
||||
result = n
|
||||
else:
|
||||
result = n
|
||||
|
||||
macro buildHtml*(tag, children: untyped): VNode =
|
||||
expectKind children, nnkDo
|
||||
var call: NimNode
|
||||
if tag.kind in nnkCallKinds:
|
||||
call = tag
|
||||
else:
|
||||
call = newCall(tag)
|
||||
call.add body(children)
|
||||
result = tcall2(call, nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
|
||||
macro buildHtml*(children: untyped): VNode =
|
||||
expectKind children, nnkDo
|
||||
result = tcall2(body(children), nil)
|
||||
when defined(debugKaraxDsl):
|
||||
echo repr result
|
||||
182
src/vdom.nim
182
src/vdom.nim
|
|
@ -1,182 +0,0 @@
|
|||
# Virtual DOM implementation
|
||||
|
||||
from dom import Event
|
||||
import shash
|
||||
|
||||
type
|
||||
VNodeKind* {.pure.} = enum
|
||||
text, int, bool, thunk,
|
||||
anchor,
|
||||
tdiv,
|
||||
table, tr, td, th, thead, tbody,
|
||||
link, span, label, br, select, option,
|
||||
fieldset, input, button, ul, li, section, header, footer,
|
||||
h1, h2, h3, h4, h5, strong
|
||||
|
||||
const
|
||||
toTag*: array[VNodeKind, cstring] = [
|
||||
cstring"#text", "#int", "#bool", "#thunk", "A", "DIV", "TABLE", "TR", "TD",
|
||||
"TH", "THEAD",
|
||||
"TBODY", "LINK",
|
||||
"SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON", "UL",
|
||||
"LI", "SECTION", "HEADER", "FOOTER", "H1", "H2", "H3", "H4", "H5", "STRONG"
|
||||
]
|
||||
|
||||
type
|
||||
EventKind* {.pure.} = enum
|
||||
onclick, ondblclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll
|
||||
|
||||
const
|
||||
toEventName*: array[EventKind, cstring] = [
|
||||
cstring"click", "dblclick", "keyup", "keydown", "keypressed", "blur",
|
||||
"change", "scroll"
|
||||
]
|
||||
|
||||
type
|
||||
EventHandler* = proc (ev: Event; target: VNode) {.closure.}
|
||||
VKey* = int
|
||||
VNode* = ref object
|
||||
kind*: VNodeKind
|
||||
key*: VKey
|
||||
id*, class*, text*: cstring
|
||||
kids: seq[VNode]
|
||||
# even index: key, odd index: value; done this way for memory efficiency:
|
||||
attrs: seq[cstring]
|
||||
events*: seq[(EventKind, EventHandler)]
|
||||
thunk*: proc (args: seq[VNode]): VNode
|
||||
hash*: Hash
|
||||
validHash*: bool
|
||||
|
||||
proc value*(n: VNode): cstring = n.text
|
||||
proc `value=`*(n: VNode; v: cstring) = n.text = v
|
||||
|
||||
proc intValue*(n: VNode): int = n.key
|
||||
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, key: i)
|
||||
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, key: ord(b))
|
||||
proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, key: -1, text: x)
|
||||
|
||||
proc callThunk*(n: VNode): VNode =
|
||||
assert n.kind == VNodeKind.thunk
|
||||
result = n.thunk(n.kids)
|
||||
|
||||
proc thunk*(cb: proc(args: seq[VNode]): VNode; args: varargs[VNode, vn]): VNode =
|
||||
VNode(kind: VNodeKind.thunk, thunk: cb, kids: @args)
|
||||
|
||||
proc eq*(a, b: VNode): bool =
|
||||
if a.kind != b.kind: return false
|
||||
if a.id != b.id: return false
|
||||
if a.class != b.class: return false
|
||||
if a.kind != VNodeKind.text:
|
||||
if a.kids.len != b.kids.len: return false
|
||||
for i in 0..<a.kids.len:
|
||||
if not eq(a.kids[i], b.kids[i]): return false
|
||||
else:
|
||||
if a.text != b.text: return false
|
||||
if a.attrs.len != b.attrs.len: return false
|
||||
for i in 0..<a.attrs.len:
|
||||
if a.attrs[i] != b.attrs[i]: return false
|
||||
result = true
|
||||
|
||||
proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
|
||||
if n.attrs.isNil:
|
||||
n.attrs = @[key, val]
|
||||
else:
|
||||
for i in countup(0, n.attrs.len-2, 2):
|
||||
if n.attrs[i] == key:
|
||||
n.attrs[i+1] = val
|
||||
return
|
||||
n.attrs.add key
|
||||
n.attrs.add val
|
||||
|
||||
proc getAttr*(n: VNode; key: cstring): cstring =
|
||||
for i in countup(0, n.attrs.len-2, 2):
|
||||
if n.attrs[i] == key: return n.attrs[i+1]
|
||||
|
||||
proc len*(x: VNode): int = x.kids.len
|
||||
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
|
||||
proc add*(parent, kid: VNode) = parent.kids.add kid
|
||||
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1)
|
||||
|
||||
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
|
||||
result = newVNode(kind)
|
||||
for k in kids: result.add k
|
||||
|
||||
proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)];
|
||||
kids: varargs[VNode]): VNode =
|
||||
result = tree(kind, kids)
|
||||
for a in attrs: result.setAttr(a[0], a[1])
|
||||
|
||||
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), key: -1)
|
||||
proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, key: -1)
|
||||
|
||||
iterator items*(n: VNode): VNode =
|
||||
for i in 0..<n.kids.len: yield n.kids[i]
|
||||
|
||||
iterator attrs*(n: VNode): (cstring, cstring) =
|
||||
for i in countup(0, n.attrs.len-2, 2):
|
||||
yield (n.attrs[i], n.attrs[i+1])
|
||||
|
||||
proc sameAttrs*(a, b: VNode): bool =
|
||||
if a.attrs.len == b.attrs.len:
|
||||
result = true
|
||||
for i in 0 ..< a.attrs.len:
|
||||
if a.attrs[i] != b.attrs[i]: return false
|
||||
|
||||
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
|
||||
n.events.add((event, handler))
|
||||
|
||||
template toStringAttr(field) =
|
||||
if n.field != nil:
|
||||
result.add " " & astToStr(field) & " = " & $n.field
|
||||
|
||||
proc toString*(n: VNode; result: var string; indent: int) =
|
||||
for i in 1..indent: result.add ' '
|
||||
if result.len > 0: result.add '\L'
|
||||
result.add "<" & $n.kind
|
||||
toStringAttr(id)
|
||||
toStringAttr(class)
|
||||
for k, v in attrs(n):
|
||||
result.add " " & $k & " = " & $v
|
||||
result.add ">\L"
|
||||
if n.kind == VNodeKind.text:
|
||||
result.add n.text
|
||||
else:
|
||||
if n.text != nil:
|
||||
result.add " value = "
|
||||
result.add n.text
|
||||
for child in items(n):
|
||||
toString(child, result, indent+2)
|
||||
for i in 1..indent: result.add ' '
|
||||
result.add "\L</" & $n.kind & ">"
|
||||
|
||||
proc calcHash*(n: VNode) =
|
||||
if n.validHash: return
|
||||
n.validHash = true
|
||||
var h: Hash = ord n.kind
|
||||
if n.id != nil:
|
||||
h &= "id"
|
||||
h &= n.id
|
||||
if n.class != nil:
|
||||
h &= "class"
|
||||
h &= n.class
|
||||
if n.key >= 0:
|
||||
h &= "k"
|
||||
h &= n.key
|
||||
for k, v in attrs(n):
|
||||
h &= " "
|
||||
h &= k
|
||||
h &= "="
|
||||
h &= v
|
||||
if n.kind == VNodeKind.text or n.text != nil:
|
||||
h &= "t"
|
||||
h &= n.text
|
||||
else:
|
||||
for child in items(n):
|
||||
calcHash(child)
|
||||
h &= child.hash
|
||||
n.hash = h
|
||||
|
||||
proc `$`*(n: VNode): cstring =
|
||||
var res = ""
|
||||
toString(n, res, 0)
|
||||
result = cstring(res)
|
||||
133
tests/blur.nim
Normal file
133
tests/blur.nim
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
|
||||
|
||||
type TextInput* = ref object of VComponent
|
||||
value, guid: cstring
|
||||
isActive: bool
|
||||
onchange: proc (value: cstring)
|
||||
|
||||
proc render(x: VComponent): VNode =
|
||||
let self = TextInput(x)
|
||||
|
||||
let style = style(
|
||||
(StyleAttr.position, cstring"relative"),
|
||||
(StyleAttr.paddingLeft, cstring"10px"),
|
||||
(StyleAttr.paddingRight, cstring"5px"),
|
||||
(StyleAttr.height, cstring"30px"),
|
||||
(StyleAttr.lineHeight, cstring"30px"),
|
||||
(StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
|
||||
(StyleAttr.fontSize, cstring"12px"),
|
||||
(StyleAttr.fontWeight, cstring"600")
|
||||
).merge(self.style)
|
||||
|
||||
let inputStyle = style.merge(style(
|
||||
(StyleAttr.color, cstring"inherit"),
|
||||
(StyleAttr.fontSize, cstring"inherit"),
|
||||
(StyleAttr.fontWeight, cstring"inherit"),
|
||||
(StyleAttr.fontFamily, cstring"inherit"),
|
||||
(StyleAttr.position, cstring"absolute"),
|
||||
(StyleAttr.top, cstring"0"),
|
||||
(StyleAttr.left, cstring"0"),
|
||||
(StyleAttr.height, cstring"100%"),
|
||||
(StyleAttr.width, cstring"100%"),
|
||||
(StyleAttr.border, cstring"none"),
|
||||
(StyleAttr.backgroundColor, cstring"transparent"),
|
||||
))
|
||||
|
||||
proc flip(ev: Event; n: VNode) =
|
||||
self.isActive = not self.isActive
|
||||
echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
|
||||
markDirty(self)
|
||||
|
||||
proc onchanged(ev: Event; n: VNode) =
|
||||
if self.onchange != nil:
|
||||
self.onchange n.value
|
||||
self.value = n.value
|
||||
|
||||
result = buildHtml(tdiv(style=style)):
|
||||
input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged)
|
||||
|
||||
proc changed(current, next: VComponent): bool =
|
||||
let current = TextInput(current)
|
||||
let next = TextInput(next)
|
||||
if current.guid != next.guid:
|
||||
result = true
|
||||
else:
|
||||
result = defaultChangedImpl(current, next)
|
||||
|
||||
proc update(current, next: VComponent) =
|
||||
let current = TextInput(current)
|
||||
let next = TextInput(next)
|
||||
current.value = next.value
|
||||
current.guid = next.guid
|
||||
#next.isActive = current.isActive
|
||||
|
||||
proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"",
|
||||
onchange: proc(v: cstring) = nil): TextInput =
|
||||
result = newComponent(TextInput, render, changed=changed, updated=update)
|
||||
result.style = style
|
||||
result.value = value
|
||||
result.onchange = onchange
|
||||
result.guid = guid
|
||||
|
||||
when false:
|
||||
type
|
||||
Combined = ref object of VComponent
|
||||
a, b: TextInput
|
||||
|
||||
proc renderComb(self: VComponent): VNode =
|
||||
let self = Combined(self)
|
||||
|
||||
proc bu(ev: Event; n: VNode) =
|
||||
self.a.value = ""
|
||||
self.b.value = ""
|
||||
markDirty(self.a)
|
||||
markDirty(self.b)
|
||||
|
||||
result = buildHtml(tdiv(style=self.style)):
|
||||
self.a
|
||||
self.b
|
||||
button(onclick=bu):
|
||||
text "reset"
|
||||
|
||||
proc changed(self: VComponent): bool =
|
||||
let self = Combined(self)
|
||||
result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
|
||||
|
||||
proc newCombined*(style: VStyle = VStyle()): Combined =
|
||||
result = newComponent(Combined, renderComb, changed=changed)
|
||||
result.a = newTextInput(style, "AAA")
|
||||
result.b = newTextInput(style, "BBB")
|
||||
|
||||
|
||||
var
|
||||
persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
|
||||
selected = -1
|
||||
errmsg = cstring""
|
||||
|
||||
proc renderPerson(text: cstring, index: int): VNode =
|
||||
proc select(ev: Event, n: VNode) =
|
||||
selected = index
|
||||
errmsg = ""
|
||||
|
||||
result = buildHtml():
|
||||
tdiv:
|
||||
text text
|
||||
button(onClick = select)
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
tdiv:
|
||||
for index, text in persons.pairs:
|
||||
renderPerson(text, index)
|
||||
tdiv:
|
||||
newTextInput(VStyle(), &selected, if selected >= 0: persons[selected] else: "", proc (v: cstring) =
|
||||
if v.len > 0:
|
||||
if selected >= 0: persons[selected] = v
|
||||
errmsg = ""
|
||||
else:
|
||||
errmsg = "name must not be empty"
|
||||
)
|
||||
tdiv:
|
||||
text errmsg
|
||||
|
||||
setRenderer createDom
|
||||
18
tests/compiler_tests.nim
Normal file
18
tests/compiler_tests.nim
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
include ../karax/prelude
|
||||
|
||||
block:
|
||||
type
|
||||
TestEnum {.pure.} = enum
|
||||
ValA, ValB
|
||||
|
||||
var e = TestEnum.ValA
|
||||
|
||||
proc renderDom(): VNode {.used.} =
|
||||
result = buildHtml():
|
||||
tdiv():
|
||||
case e
|
||||
of TestEnum.ValA:
|
||||
tdiv: text "A"
|
||||
of TestEnum.ValB:
|
||||
tdiv: text "B"
|
||||
|
||||
128
tests/components.nim
Normal file
128
tests/components.nim
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
|
||||
|
||||
type TextInput* = ref object of VComponent
|
||||
value: cstring
|
||||
isActive: bool
|
||||
|
||||
proc render(x: VComponent): VNode =
|
||||
let self = TextInput(x)
|
||||
|
||||
let style = style(
|
||||
(StyleAttr.position, cstring"relative"),
|
||||
(StyleAttr.paddingLeft, cstring"10px"),
|
||||
(StyleAttr.paddingRight, cstring"5px"),
|
||||
(StyleAttr.height, cstring"30px"),
|
||||
(StyleAttr.lineHeight, cstring"30px"),
|
||||
(StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
|
||||
(StyleAttr.fontSize, cstring"12px"),
|
||||
(StyleAttr.fontWeight, cstring"600")
|
||||
).merge(self.style)
|
||||
|
||||
let inputStyle = style.merge(style(
|
||||
(StyleAttr.color, cstring"inherit"),
|
||||
(StyleAttr.fontSize, cstring"inherit"),
|
||||
(StyleAttr.fontWeight, cstring"inherit"),
|
||||
(StyleAttr.fontFamily, cstring"inherit"),
|
||||
(StyleAttr.position, cstring"absolute"),
|
||||
(StyleAttr.top, cstring"0"),
|
||||
(StyleAttr.left, cstring"0"),
|
||||
(StyleAttr.height, cstring"100%"),
|
||||
(StyleAttr.width, cstring"100%"),
|
||||
(StyleAttr.border, cstring"none"),
|
||||
(StyleAttr.backgroundColor, cstring"transparent"),
|
||||
))
|
||||
|
||||
proc flip(ev: Event; n: VNode) =
|
||||
self.isActive = not self.isActive
|
||||
echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
|
||||
markDirty(self)
|
||||
|
||||
result = buildHtml(tdiv(style=style)):
|
||||
input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip,
|
||||
events=self.events)
|
||||
|
||||
proc update(current, next: VComponent) =
|
||||
let current = TextInput(current)
|
||||
let next = TextInput(next)
|
||||
current.value = next.value
|
||||
current.key = next.key
|
||||
|
||||
proc changed(current, next: VComponent): bool = true
|
||||
|
||||
proc newTextInput*(style: VStyle = VStyle(); key: cstring;
|
||||
value = cstring""): TextInput =
|
||||
result = newComponent(TextInput, render, changed=changed, updated=update)
|
||||
result.style = style
|
||||
result.value = value
|
||||
#result.key = key
|
||||
|
||||
when false:
|
||||
type
|
||||
Combined = ref object of VComponent
|
||||
a, b: TextInput
|
||||
|
||||
proc renderComb(self: VComponent): VNode =
|
||||
let self = Combined(self)
|
||||
|
||||
proc bu(ev: Event; n: VNode) =
|
||||
self.a.value = ""
|
||||
self.b.value = ""
|
||||
markDirty(self.a)
|
||||
markDirty(self.b)
|
||||
|
||||
result = buildHtml(tdiv(style=self.style)):
|
||||
self.a
|
||||
self.b
|
||||
button(onclick=bu):
|
||||
text "reset"
|
||||
|
||||
proc changed(self: VComponent): bool =
|
||||
let self = Combined(self)
|
||||
result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b)
|
||||
|
||||
proc newCombined*(style: VStyle = VStyle()): Combined =
|
||||
result = newComponent(Combined, renderComb, changed=changed)
|
||||
result.a = newTextInput(style, "AAA")
|
||||
result.b = newTextInput(style, "BBB")
|
||||
|
||||
|
||||
var
|
||||
persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"]
|
||||
errmsg = cstring""
|
||||
|
||||
proc renderPerson(text: cstring, index: int): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
newTextInput(VStyle(), &index, text):
|
||||
proc onkeyuplater(ev: Event; n: VNode) =
|
||||
let v = n.value
|
||||
if v.len > 0:
|
||||
persons[index] = v
|
||||
errmsg = ""
|
||||
else:
|
||||
errmsg = "name must not be empty"
|
||||
button:
|
||||
proc onclick(ev: Event; n: VNode) =
|
||||
persons.delete(index)
|
||||
errmsg = ""
|
||||
echo persons
|
||||
text "(x)"
|
||||
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
tdiv:
|
||||
for index, text in persons.pairs:
|
||||
renderPerson(text, index)
|
||||
tdiv:
|
||||
newTextInput(VStyle(), &persons.len, ""):
|
||||
proc onkeyupenter(ev: Event; n: VNode) =
|
||||
let v = n.value
|
||||
if v.len > 0:
|
||||
persons.add v
|
||||
errmsg = ""
|
||||
else:
|
||||
errmsg = "name must not be empty"
|
||||
tdiv:
|
||||
text errmsg
|
||||
|
||||
setRenderer createDom
|
||||
|
|
@ -3,14 +3,13 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Diff dom tests</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
|
||||
<div id="ROOT" />
|
||||
|
||||
<script type="text/javascript" src="nimcache/diffDomTests.js"></script>
|
||||
<script type="text/javascript" src="diffDomTests.js"></script>
|
||||
<script type="text/javascript">
|
||||
onload(window.location.pathname.split('/')[1])
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,183 +1,107 @@
|
|||
|
||||
import vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
|
||||
import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
|
||||
|
||||
var
|
||||
entries: seq[cstring]
|
||||
results: seq[cstring]
|
||||
timeout : Timeout
|
||||
entries: seq[cstring]
|
||||
results: seq[cstring]
|
||||
|
||||
proc reset() =
|
||||
results.add cstring"reset started"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
|
||||
redraw()
|
||||
results.add cstring"reset finished"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
||||
redrawSync()
|
||||
|
||||
proc checkOrder(order : seq[int]): bool =
|
||||
var ul = getElementById("ul")
|
||||
if ul == nil or len(ul.children) != len(order):
|
||||
return false
|
||||
var pos = 0
|
||||
for child in ul.children:
|
||||
if child.id != $order[pos]:
|
||||
return false
|
||||
inc pos
|
||||
return true
|
||||
proc checkOrder(order: seq[int]): bool =
|
||||
var ul = getElementById("ul")
|
||||
if ul == nil or len(ul.children) != len(order):
|
||||
kout ul, len(order)
|
||||
return false
|
||||
var pos = 0
|
||||
for child in ul.children:
|
||||
if child.id != $order[pos]:
|
||||
kout pos
|
||||
return false
|
||||
inc pos
|
||||
return true
|
||||
|
||||
proc check1() =
|
||||
let result = checkOrder(@[0, 1, 2, 3, 4, 7, 5])
|
||||
if result:
|
||||
results.add cstring"test1 - OK"
|
||||
else:
|
||||
results.add cstring"test1 - FAIL"
|
||||
proc check(name: cstring; order: seq[int]) =
|
||||
let result = checkOrder(order)
|
||||
results.add name & (if result: cstring" - OK" else: cstring" -FAIL")
|
||||
|
||||
# result: 0 1 2 3 4 7 5
|
||||
proc test1() =
|
||||
results.add cstring"test1 started"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
|
||||
entries.insert(cstring("7"), 5)
|
||||
redraw()
|
||||
timeout = setTimeout(check1, 20)
|
||||
results.add cstring"test1 started"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
||||
entries.insert(cstring("7"), 5)
|
||||
redrawSync()
|
||||
check("test1", @[0, 1, 2, 3, 4, 7, 5])
|
||||
|
||||
proc check2() =
|
||||
let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5])
|
||||
if result:
|
||||
results.add cstring"test1 - OK"
|
||||
else:
|
||||
results.add cstring"test2 - FAIL"
|
||||
|
||||
# result: 8 0 1 2 3 4 7 5
|
||||
proc test2() =
|
||||
results.add cstring"test2 started"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ]
|
||||
entries.insert(cstring("7"), 5)
|
||||
entries.insert(cstring("8"), 0)
|
||||
redraw()
|
||||
timeout = setTimeout(check2, 20)
|
||||
results.add cstring"test2 started"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
||||
entries.insert(cstring("7"), 5)
|
||||
entries.insert(cstring("8"), 0)
|
||||
redrawSync()
|
||||
check("test2", @[8, 0, 1, 2, 3, 4, 7, 5])
|
||||
|
||||
proc check3() =
|
||||
let result = checkOrder(@[2, 3, 4, 1])
|
||||
if result:
|
||||
results.add cstring"test3 - OK"
|
||||
else:
|
||||
results.add cstring"test3 - FAIL"
|
||||
|
||||
# result: 2 3 4 1
|
||||
proc test3() =
|
||||
results.add cstring"test3 started"
|
||||
entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1") ]
|
||||
redraw()
|
||||
timeout = setTimeout(check3, 20)
|
||||
results.add cstring"test3 started"
|
||||
entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1")]
|
||||
redrawSync()
|
||||
check("test3", @[2, 3, 4, 1])
|
||||
|
||||
proc check4() =
|
||||
let result = checkOrder(@[5, 6, 7, 8])
|
||||
if result:
|
||||
results.add cstring"test4 - OK"
|
||||
else:
|
||||
results.add cstring"test4 - FAIL"
|
||||
|
||||
# result: 5 6 7 8
|
||||
proc test4() =
|
||||
results.add cstring"test4 started"
|
||||
entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ]
|
||||
redraw()
|
||||
timeout = setTimeout(check4, 20)
|
||||
results.add cstring"test4 started"
|
||||
entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ]
|
||||
redrawSync()
|
||||
check("test4", @[5, 6, 7, 8])
|
||||
|
||||
proc check5() =
|
||||
let result = checkOrder(@[0, 1, 3, 5, 4, 5])
|
||||
if result:
|
||||
results.add cstring"test5 - OK"
|
||||
else:
|
||||
results.add cstring"test5 - FAIL"
|
||||
|
||||
# result: 0 1 3 5 4 5
|
||||
proc test5() =
|
||||
results.add cstring"test5 started"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ]
|
||||
redraw()
|
||||
timeout = setTimeout(check5, 20)
|
||||
results.add cstring"test5 started"
|
||||
entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5")]
|
||||
redrawSync()
|
||||
check("test 5", @[0, 1, 3, 5, 4, 5])
|
||||
|
||||
proc check6() =
|
||||
let result = checkOrder(@[])
|
||||
if result:
|
||||
results.add cstring"test6 - OK"
|
||||
else:
|
||||
results.add cstring"test6 - FAIL"
|
||||
|
||||
# result: empty
|
||||
proc test6() =
|
||||
results.add cstring"test6 started"
|
||||
entries = @[]
|
||||
redraw()
|
||||
timeout = setTimeout(check6, 20)
|
||||
|
||||
proc check7() =
|
||||
let result = checkOrder(@[2])
|
||||
if result:
|
||||
results.add cstring"test7 - OK"
|
||||
else:
|
||||
results.add cstring"test7 - FAIL"
|
||||
redraw()
|
||||
results.add cstring"test6 started"
|
||||
entries = @[]
|
||||
redrawSync()
|
||||
check("test 6", @[])
|
||||
|
||||
# result: 2
|
||||
proc test7() =
|
||||
results.add cstring"test7 started"
|
||||
entries = @[cstring("2")]
|
||||
redraw()
|
||||
timeout = setTimeout(check7, 20)
|
||||
results.add cstring"test7 started"
|
||||
entries = @[cstring("2")]
|
||||
redrawSync()
|
||||
check("test 7", @[2])
|
||||
|
||||
proc createEntry(id: int): VNode =
|
||||
result = buildHtml():
|
||||
button(id="" & $id):
|
||||
text $id
|
||||
text $id
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv()):
|
||||
ul(id="ul"):
|
||||
for e in entries:
|
||||
createEntry(parseInt(e))
|
||||
for r in results:
|
||||
tdiv:
|
||||
text r
|
||||
result = buildHtml(tdiv()):
|
||||
ul(id="ul"):
|
||||
for e in entries:
|
||||
createEntry(parseInt(e))
|
||||
for r in results:
|
||||
tdiv:
|
||||
text r
|
||||
|
||||
proc onload() =
|
||||
for i in 0..5: # 0_000:
|
||||
entries.add(cstring($i))
|
||||
test1()
|
||||
reset()
|
||||
test2()
|
||||
reset()
|
||||
test3()
|
||||
reset()
|
||||
test4()
|
||||
reset()
|
||||
test5()
|
||||
reset()
|
||||
test6()
|
||||
reset()
|
||||
test7()
|
||||
|
||||
setRenderer createDom
|
||||
|
||||
proc onload(session: cstring) {.exportc.} =
|
||||
for i in 0..5: # 0_000:
|
||||
entries.add(cstring($i))
|
||||
init()
|
||||
|
||||
var dtReset = 100
|
||||
var dtTest = 500
|
||||
|
||||
var t = dtReset
|
||||
timeout = setTimeout(test1, t)
|
||||
t += dtTest
|
||||
timeout = setTimeout(reset, t)
|
||||
t += dtReset
|
||||
|
||||
timeout = setTimeout(test2, t)
|
||||
t += dtTest
|
||||
timeout = setTimeout(reset, t)
|
||||
t += dtReset
|
||||
|
||||
timeout = setTimeout(test3, t)
|
||||
t += dtTest
|
||||
timeout = setTimeout(reset, t)
|
||||
t += dtReset
|
||||
|
||||
timeout = setTimeout(test4, t)
|
||||
t += dtTest
|
||||
timeout = setTimeout(reset, t)
|
||||
t += dtReset
|
||||
|
||||
timeout = setTimeout(test5, t)
|
||||
t += dtTest
|
||||
timeout = setTimeout(reset, t)
|
||||
t += dtReset
|
||||
|
||||
timeout = setTimeout(test6, t)
|
||||
t += dtTest
|
||||
timeout = setTimeout(reset, t)
|
||||
t += dtReset
|
||||
|
||||
timeout = setTimeout(test7, t)
|
||||
onload()
|
||||
|
|
|
|||
185
tests/difftest.nim
Normal file
185
tests/difftest.nim
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
|
||||
include "../karax/karax"
|
||||
import "../karax/karaxdsl"
|
||||
|
||||
proc hasDom(n: Vnode) =
|
||||
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
|
||||
discard
|
||||
else:
|
||||
doAssert n.dom != nil
|
||||
for i in 0..<n.len: hasDom(n[i])
|
||||
|
||||
proc shortRepr(n: VNode): string =
|
||||
if n == nil:
|
||||
result = "nil"
|
||||
elif n.kind == VNodeKind.text:
|
||||
result = $n.text
|
||||
else:
|
||||
result = $n.kind
|
||||
for i in 0..<n.len:
|
||||
result &= " " & shortRepr(n[i])
|
||||
|
||||
var err = 0
|
||||
|
||||
proc doDiff(a, b: VNode; expected: varargs[string]) =
|
||||
diff(b, a, nil, vnodeToDom(a, kxi), kxi)
|
||||
var j = 0
|
||||
for i in 0..<kxi.patchLen:
|
||||
if kxi.patches[i].k != pkSame:
|
||||
let n = if kxi.patches[i].k == pkDetach: kxi.patches[i].oldNode else: kxi.patches[i].newNode
|
||||
let p = $kxi.patches[i].k & " " & shortRepr(n)
|
||||
if j >= expected.len:
|
||||
echo "patches differ; expected nothing but got: ", p
|
||||
inc err
|
||||
elif p != expected[j]:
|
||||
echo "patches differ; expected ", expected[i], " but got: ", p
|
||||
inc err
|
||||
inc j
|
||||
#hasDom(kxi.currentTree)
|
||||
kxi.patchLen = 0
|
||||
|
||||
proc testAppend() =
|
||||
let a = buildHtml(tdiv):
|
||||
ul:
|
||||
li: text "A"
|
||||
li: text "B"
|
||||
let b = buildHtml(tdiv):
|
||||
ul:
|
||||
li: text "A"
|
||||
li: text "B"
|
||||
li:
|
||||
tdiv:
|
||||
text "C"
|
||||
doDiff(a, b, "pkAppend li div C")
|
||||
|
||||
proc testInsert() =
|
||||
let a = buildHtml(tdiv):
|
||||
ul:
|
||||
li: text "A"
|
||||
li:
|
||||
button:
|
||||
text "C"
|
||||
let b = buildHtml(tdiv):
|
||||
ul:
|
||||
li: text "A"
|
||||
li: text "B"
|
||||
li:
|
||||
button:
|
||||
text "C"
|
||||
doDiff(a, b, "pkInsertBefore li B")
|
||||
|
||||
proc testInsert2() =
|
||||
let a = buildHtml(tdiv):
|
||||
tdiv:
|
||||
tdiv:
|
||||
ul:
|
||||
li: text "A"
|
||||
li:
|
||||
button:
|
||||
text "D"
|
||||
let b = buildHtml(tdiv):
|
||||
tdiv:
|
||||
tdiv:
|
||||
ul:
|
||||
li: text "A"
|
||||
li: text "B"
|
||||
li: text "C"
|
||||
li:
|
||||
button:
|
||||
text "D"
|
||||
doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C")
|
||||
|
||||
proc testDelete() =
|
||||
let a = buildHtml(tdiv):
|
||||
ul:
|
||||
li: text "A"
|
||||
li: text "B"
|
||||
li: text "C"
|
||||
let b = buildHtml(tdiv):
|
||||
ul:
|
||||
discard
|
||||
doDiff(a, b, "pkDetach li A", "pkRemove nil",
|
||||
"pkDetach li B", "pkRemove nil",
|
||||
"pkDetach li C", "pkRemove nil")
|
||||
|
||||
proc testDeleteMiddle() =
|
||||
let a = buildHtml(tdiv):
|
||||
ul:
|
||||
li:
|
||||
tdiv: text "A"
|
||||
li:
|
||||
tdiv: text "B"
|
||||
li:
|
||||
tdiv: text "C"
|
||||
li:
|
||||
tdiv: text "D"
|
||||
li:
|
||||
tdiv: text "E"
|
||||
li:
|
||||
tdiv: text "F"
|
||||
li:
|
||||
tdiv: text "G"
|
||||
li:
|
||||
button:
|
||||
tdiv: text "H"
|
||||
let b = buildHtml(tdiv):
|
||||
ul:
|
||||
li:
|
||||
tdiv: text "A"
|
||||
li:
|
||||
tdiv: text "B"
|
||||
li:
|
||||
tdiv: text "C"
|
||||
li:
|
||||
tdiv: text "D"
|
||||
li:
|
||||
tdiv: text "E"
|
||||
li:
|
||||
tdiv: text "F"
|
||||
li:
|
||||
button:
|
||||
tdiv: text "H"
|
||||
doDiff(a, b, "pkDetach li div G", "pkRemove nil")
|
||||
|
||||
proc createEntry(id: cstring): VNode =
|
||||
result = buildHtml():
|
||||
button(id="" & id):
|
||||
text id
|
||||
|
||||
proc createEntries(entries: seq[cstring]): VNode =
|
||||
result = buildHtml(tdiv()):
|
||||
ul(id="ul"):
|
||||
for e in entries:
|
||||
createEntry(e)
|
||||
for r in entries:
|
||||
tdiv:
|
||||
text r
|
||||
|
||||
proc testWild() =
|
||||
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
|
||||
let a = createEntries(entries)
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
||||
let b = createEntries(entries)
|
||||
doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil")
|
||||
|
||||
proc testWildInsert() =
|
||||
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
||||
let a = createEntries(entries)
|
||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
|
||||
let b = createEntries(entries)
|
||||
doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7")
|
||||
|
||||
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (data: RouterData): VNode = discard,
|
||||
byId: newJDict[cstring, VNode]())
|
||||
|
||||
testAppend()
|
||||
testInsert()
|
||||
testInsert2()
|
||||
testDelete()
|
||||
testWild()
|
||||
testWildInsert()
|
||||
testDeleteMiddle()
|
||||
if err == 0:
|
||||
echo "Success"
|
||||
else:
|
||||
quit 1
|
||||
33
tests/domEventTests.css
Normal file
33
tests/domEventTests.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#dragDiv {
|
||||
background-color: #0d1224;
|
||||
width: 500px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 500px;
|
||||
left: 200px;
|
||||
text-align: center;
|
||||
cursor: move;
|
||||
color: rgb(60, 201, 163);
|
||||
}
|
||||
|
||||
#results {
|
||||
font-family: monospace;
|
||||
font-size: 10pt;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
background-color: #000000;
|
||||
color: forestgreen;
|
||||
}
|
||||
|
||||
#test10input1, #test10input2, #test11h3, #test12input {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#test11h3 {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: dimgrey;
|
||||
}
|
||||
13
tests/domEventTests.html
Normal file
13
tests/domEventTests.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DOM Event Tests</title>
|
||||
<link href="domEventTests.css" rel="stylesheet" />
|
||||
<meta charset="UTF-8" />
|
||||
<script src="jquery-3.2.1.js" type="text/javascript"></script>
|
||||
</head>
|
||||
<body id="body">
|
||||
<div id="ROOT"></div>
|
||||
<script src="domEventTests.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
624
tests/domEventTests.nim
Normal file
624
tests/domEventTests.nim
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
import times, jstrutils, strutils, kdom, karax, karaxdsl, vdom, tables
|
||||
|
||||
var
|
||||
results: seq[cstring]
|
||||
dragEventsCompleted = initTable[DragEventTypes, bool]()
|
||||
testRunning: array[13, bool]
|
||||
testCompleted: array[13, bool]
|
||||
test2ImageDrop = false
|
||||
dblClickCount = 0
|
||||
test6ClickCount = 0
|
||||
test8Failure = false
|
||||
test10inputFocusCount = 0
|
||||
test12Failure = false
|
||||
|
||||
proc getLogText(text: string): cstring =
|
||||
result = "[$#]: $#" % [format(getLocalTime(getTime()), "HH:mm:ss"), $text]
|
||||
|
||||
proc log(text: string, clear: bool = false) =
|
||||
if clear:
|
||||
results = @[]
|
||||
results.add getLogText(text)
|
||||
echo text
|
||||
redrawSync()
|
||||
|
||||
# Scroll to top after adding text using JQuery
|
||||
discard window.setTimeout("$('#results').animate({ scrollTop: 0 }, 100);", 100)
|
||||
|
||||
proc check(cond: bool, msg: string) =
|
||||
if not cond:
|
||||
log "FAIL: " & msg
|
||||
doAssert(cond, msg)
|
||||
|
||||
proc check(cond: bool, msg: string, warn: bool): bool =
|
||||
if not cond:
|
||||
if warn:
|
||||
log "WARNING: " & msg
|
||||
else:
|
||||
log "FAIL: " & msg
|
||||
doAssert(cond, msg)
|
||||
result = cond
|
||||
|
||||
proc dragDivDragStart(ev: kdom.Event) =
|
||||
dragEventsCompleted[DragStart] = true
|
||||
var dgev = cast[DragEvent](ev)
|
||||
var dragDiv = document.getElementById("dragDiv")
|
||||
|
||||
if dragDiv != nil:
|
||||
var style = window.getComputedStyle(dgev.target)
|
||||
var dataX = $(parseInt(style.left) - dgev.clientX)
|
||||
var dataY = $(parseInt(style.top) - dgev.clientY)
|
||||
dgev.dataTransfer.setData("text/plain", dataX & "," & dataY)
|
||||
var img = (ImageElement)document.createElement("img")
|
||||
img.src ="https://nim-lang.org/assets/img/logo.svg"
|
||||
dgev.dataTransfer.setDragImage(img, 0, 0)
|
||||
|
||||
proc dragDivDragOver(ev: kdom.Event) =
|
||||
dragEventsCompleted[DragOver] = true
|
||||
ev.preventDefault()
|
||||
|
||||
proc checkAllTests() =
|
||||
var allTestsPassed = true
|
||||
for a in testCompleted:
|
||||
if not a:
|
||||
allTestsPassed = false
|
||||
break
|
||||
|
||||
if allTestsPassed:
|
||||
log "All tests completed successfully!"
|
||||
else:
|
||||
log "Some tests failed. Check the log."
|
||||
|
||||
proc completeTest13() =
|
||||
testRunning[12] = false
|
||||
testCompleted[12] = true
|
||||
log "Test 13 completed successfully."
|
||||
checkAllTests()
|
||||
|
||||
proc test13() =
|
||||
testRunning[12] = true
|
||||
log "Test 13 started."
|
||||
log "This test verifies KeyboardEvent getModifierState functionality."
|
||||
log "To continue, press the left CTRL key."
|
||||
|
||||
proc test12focus1(ev: kdom.Event) =
|
||||
ev.stopImmediatePropagation()
|
||||
|
||||
proc test12focus2(ev: kdom.Event) =
|
||||
test12Failure = true
|
||||
check false, "Unexpected listener invocation."
|
||||
|
||||
proc test12focus3(ev: kdom.Event) =
|
||||
test12Failure = true
|
||||
check false, "Unexpected listener invocation."
|
||||
|
||||
proc completeTest12() =
|
||||
if not test12Failure:
|
||||
var elmt = document.getElementById("test12input")
|
||||
elmt.style.visibility = "hidden"
|
||||
testRunning[11] = false
|
||||
testCompleted[11] = true
|
||||
log "Test 12 completed successfully."
|
||||
test13()
|
||||
|
||||
proc test12() =
|
||||
testRunning[11] = true
|
||||
log "Test 12 started."
|
||||
log "This test verifies stopImmediatePropagation functionality."
|
||||
|
||||
var elmt = document.getElementById("test12input")
|
||||
elmt.style.visibility = "visible"
|
||||
elmt.addEventListener($Focus, test12focus1)
|
||||
elmt.addEventListener($Focus, test12focus2)
|
||||
elmt.addEventListener($Focus, test12focus3)
|
||||
elmt.focus()
|
||||
discard setTimeout(completeTest12, 500)
|
||||
|
||||
proc completeTest11() =
|
||||
document.querySelector("h3").style.visibility = "hidden"
|
||||
|
||||
testRunning[10] = false
|
||||
testCompleted[10] = true
|
||||
log "Test 11 completed."
|
||||
test12()
|
||||
|
||||
proc test11() =
|
||||
testRunning[10] = true
|
||||
log "Test 11 started."
|
||||
log "This test checks querySelector and querySelectorAll."
|
||||
|
||||
document.getElementById("test11h3").style.visibility = "visible"
|
||||
|
||||
var elmt = document.querySelector("h3")
|
||||
check elmt != nil, "querySelector returned nil."
|
||||
log "text: " & $elmt.innerHTML
|
||||
check $elmt.innerHTML == "test 11", "Unexpected h3 value."
|
||||
|
||||
var elmts = document.querySelectorAll("h3")
|
||||
log "elmts len: " & $elmts.len
|
||||
check elmts.len == 1, "Unexpected elmts len."
|
||||
|
||||
log "text: " & $elmts[0].innerHTML
|
||||
check $elmts[0].innerHTML == "test 11", "Unexpected h3 value."
|
||||
|
||||
discard setTimeout(completeTest11, 500)
|
||||
|
||||
proc completeTest10() =
|
||||
testRunning[9] = false
|
||||
testCompleted[9] = true
|
||||
var test10input1 = document.getElementById("test10input1")
|
||||
var test10input2 = document.getElementById("test10input2")
|
||||
test10input1.style.visibility = "hidden"
|
||||
test10input2.style.visibility = "hidden"
|
||||
log "Test 10 completed."
|
||||
test11()
|
||||
|
||||
proc test10inputOnfocus(ev: kdom.Event) =
|
||||
if testRunning[9]:
|
||||
inc test10inputFocusCount
|
||||
if test10inputFocusCount > 3:
|
||||
discard setTimeout(completeTest10, 500)
|
||||
|
||||
proc test10() =
|
||||
log "Test 10 started."
|
||||
log "This test checks element.focus()."
|
||||
testRunning[9] = true
|
||||
var test10input1 = document.getElementById("test10input1")
|
||||
var test10input2 = document.getElementById("test10input2")
|
||||
test10input1.style.visibility = "visible"
|
||||
test10input2.style.visibility = "visible"
|
||||
test10input1.addEventListener($DomEvent.Focus, test10inputOnfocus)
|
||||
test10input2.addEventListener($DomEvent.Focus, test10inputOnfocus)
|
||||
test10input1.focus()
|
||||
test10input2.focus()
|
||||
test10input1.focus()
|
||||
test10input2.focus()
|
||||
|
||||
proc completeTest9() =
|
||||
var test9Value = $document.getElementById("test9value").value
|
||||
doAssert(test9Value == "[test9]", "Unexpected test9 value.")
|
||||
testRunning[8] = false
|
||||
testCompleted[8] = true
|
||||
test10()
|
||||
|
||||
proc test9() =
|
||||
log "Test 9 started."
|
||||
log "This tests the setTimeout(w: Window, code: cstring, pause: int) proc."
|
||||
testRunning[8] = true
|
||||
|
||||
discard window.setTimeout("document.getElementById('test9value').value = '[test9]';", 500)
|
||||
discard window.setTimeout(completeTest9, 1000)
|
||||
|
||||
proc completeTest8() =
|
||||
if not test8Failure:
|
||||
log "Test 8 completed successfully."
|
||||
testRunning[7] = false
|
||||
testCompleted[7] = true
|
||||
test9()
|
||||
else:
|
||||
log "Test 8 failed."
|
||||
|
||||
proc failTest8() =
|
||||
test8Failure = true
|
||||
check(false, "ClearTimeout failed.")
|
||||
|
||||
proc test8() =
|
||||
log "Test 8 started."
|
||||
log "This test checks that clearTimeout() works correctly."
|
||||
testRunning[7] = true
|
||||
var timeout = setTimeout(failTest8, 1000)
|
||||
clearTimeout(timeout)
|
||||
discard setTimeout(completeTest8, 1200)
|
||||
|
||||
proc test7() =
|
||||
log "Test 7 started."
|
||||
log "This test ensures that mouse events correctly pass button and key modifier information."
|
||||
log "To continue, right-click while holding SHIFT."
|
||||
testRunning[6] = true
|
||||
|
||||
# A mouseMove handler is needed (even if empty) so mouseDown will have a value for movementX and movementY
|
||||
proc mouseMove(ev: kdom.Event) =
|
||||
discard
|
||||
|
||||
proc mouseDown(ev: kdom.Event) =
|
||||
var me = (MouseEvent)ev
|
||||
if testRunning[5]:
|
||||
log "[ME] button: " & $me.button
|
||||
log "[ME] buttons: " & $me.buttons
|
||||
log "[ME] altKey: " & $me.altKey
|
||||
log "[ME] ctrlKey: " & $me.ctrlKey
|
||||
log "[ME] metaKey: " & $me.metaKey
|
||||
log "[ME] shiftKey: " & $me.shiftKey
|
||||
|
||||
log "[ME] clientX: " & $me.clientX
|
||||
check me.clientX > 0, "Unexpected clientX value."
|
||||
|
||||
log "[ME] clientY: " & $me.clientY
|
||||
check me.clientY > 0, "Unexpected clientY value."
|
||||
|
||||
log "[ME] movementX: " & $me.movementX
|
||||
check me.movementX > 0, "Unexpected movementX value."
|
||||
|
||||
log "[ME] movementY: " & $me.movementY
|
||||
check me.movementY > 0, "Unexpected movementY value."
|
||||
|
||||
log "[ME] offsetX: " & $me.offsetX
|
||||
check me.offsetX > 0, "Unexpected offsetX value."
|
||||
|
||||
log "[ME] offsetY: " & $me.offsetY
|
||||
check me.offsetY > 0, "Unexpected offsetY value."
|
||||
|
||||
log "[ME] pageX: " & $me.pageX
|
||||
check me.pageX > 0, "Unexpected pageX value."
|
||||
|
||||
log "[ME] pageY: " & $me.pageY
|
||||
check me.pageY > 0, "Unexpected pageY value."
|
||||
|
||||
log "[ME] relatedTarget == nil: " & $(me.relatedTarget == nil)
|
||||
|
||||
log "[ME] screenX: " & $me.screenX
|
||||
check me.screenX > 0, "Unexpected screenX value."
|
||||
|
||||
log "[ME] screenY: " & $me.screenY
|
||||
check me.screenY > 0, "Unexpected screenY value."
|
||||
|
||||
log "[ME] x: " & $me.x
|
||||
check me.x > 0, "Unexpected x value."
|
||||
|
||||
log "[ME] y: " & $me.y
|
||||
check me.y > 0, "Unexpected y value."
|
||||
|
||||
if dblClickCount == 1:
|
||||
inc test6ClickCount
|
||||
|
||||
if test6ClickCount > 1:
|
||||
testRunning[5] = false
|
||||
testCompleted[5] = true
|
||||
log "Test 6 completed successfully."
|
||||
test7()
|
||||
elif testRunning[6]:
|
||||
log "[ME] buttons: " & $me.buttons
|
||||
var passed = check(me.buttons == (int)SecondaryButton, "Unexpected buttons value.", true)
|
||||
|
||||
log "[ME] shiftKey: " & $me.shiftKey
|
||||
passed = passed and check(me.shiftKey, "Unexpected shiftKey value.", true)
|
||||
|
||||
var isShiftPressed = me.getModifierState($KeyboardEventKey.Shift)
|
||||
log "[ME] getModifierState shift result: " & $isShiftPressed
|
||||
passed = passed and check(isShiftPressed, "Unexpected getModifierState shift result.", true)
|
||||
|
||||
if passed:
|
||||
log "Test 7 completed successfully."
|
||||
testRunning[6] = false
|
||||
testCompleted[6] = true
|
||||
|
||||
ev.preventDefault()
|
||||
|
||||
if testCompleted[6]:
|
||||
test8()
|
||||
|
||||
proc dblClick(ev: kdom.Event) =
|
||||
inc dblClickCount
|
||||
check dblClickCount == 1, "Unexpected double-click event invocation."
|
||||
log "Try double-clicking again."
|
||||
|
||||
proc test6() =
|
||||
log "Test 6 started."
|
||||
log "This test validates that addEventListener can be called by passing AddEventListenerOptions, and the options work correctly."
|
||||
log "To continue, try double-clicking the mouse."
|
||||
var options = AddEventListenerOptions(once:true)
|
||||
document.addEventListener($DomEvent.DblClick, dblClick, options)
|
||||
testRunning[5] = true
|
||||
|
||||
proc test5() =
|
||||
log "Test 5 started."
|
||||
log "This test validates the newly added properties: window.screen.width, window.scren.height, element.clientWidth, and element.clientHeight."
|
||||
log "window.screen.width: " & $window.screen.width
|
||||
check(window.screen.width > 0, "Unexpected value for window.screen.width.")
|
||||
|
||||
log "window.screen.height: " & $window.screen.height
|
||||
check(window.screen.height > 0, "Unexpected value for window.screen.height.")
|
||||
|
||||
var elem = document.getElementById("results")
|
||||
log "results div clientWidth: " & $elem.clientWidth
|
||||
check(elem.clientWidth > 0, "Unexpected value for clientWidth.")
|
||||
|
||||
log "results div clientHeight: " & $elem.clientHeight
|
||||
check(elem.clientHeight > 0, "Unexpected value for clientHeight.")
|
||||
|
||||
testRunning[4] = false
|
||||
testCompleted[4] = true
|
||||
|
||||
log "Test 5 completed successfully."
|
||||
test6()
|
||||
|
||||
proc keyDown(ev: kdom.Event) =
|
||||
var kbev = ((KeyboardEvent)ev)
|
||||
if testRunning[2]:
|
||||
log "[KB] altKey: " & $kbev.altKey
|
||||
log "[KB] ctrlKey: " & $kbev.ctrlKey
|
||||
log "[KB] metaKey: " & $kbev.metaKey
|
||||
log "[KB] shiftKey: " & $kbev.shiftKey
|
||||
log "[KB] code: " & $kbev.code
|
||||
log "[KB] isComposing: " & $kbev.isComposing
|
||||
log "[KB] key: " & $kbev.key
|
||||
log "[KB] keyCode: " & $kbev.keyCode
|
||||
log "[KB] location: " & $kbev.location
|
||||
|
||||
log "Test 3 completed successfully."
|
||||
testRunning[2] = false
|
||||
testCompleted[2] = true
|
||||
|
||||
testRunning[3] = true
|
||||
log "Test 4 started."
|
||||
log "This test ensures the child events have access to base event settings."
|
||||
|
||||
log "[KB] event.bubbles: " & $kbev.bubbles
|
||||
check kbev.bubbles, "Unexpected event.bubbles value."
|
||||
|
||||
log "[KB] event.cancelBubble: " & $kbev.cancelBubble
|
||||
check kbev.cancelBubble == false, "Unexpected event.cancelBubble value."
|
||||
|
||||
log "[KB] event.cancelable: " & $kbev.cancelable
|
||||
check kbev.cancelable, "Unexpected event.cancelable value."
|
||||
|
||||
log "[KB] event.composed: " & $kbev.composed
|
||||
check kbev.composed, "Unexpected event.composed value."
|
||||
|
||||
log "[KB] event.currentTarget.nodeName: " & $kbev.currentTarget.nodeName
|
||||
check kbev.currentTarget.nodeName == "#document", "Unexpected event.currentTarget.nodeName value."
|
||||
|
||||
log "[KB] event.defaultPrevented: " & $kbev.defaultPrevented
|
||||
check kbev.defaultPrevented == false, "Unexpected event.defaultPrevented value."
|
||||
|
||||
log "[KB] event.eventPhase: " & $kbev.eventPhase
|
||||
check EventPhase(kbev.eventPhase) == BubblingPhase, "Unexpected event.eventPhase value."
|
||||
|
||||
log "[KB] event.isTrusted: " & $kbev.isTrusted
|
||||
check kbev.isTrusted, "Unexpected event.isTrusted value."
|
||||
|
||||
log "[UI] kbev.detail: " & $kbev.detail
|
||||
|
||||
log "[UI] kbev.view.outerHeight: " & $kbev.view.outerHeight
|
||||
check kbev.view.outerHeight > 0, "Unexpected view.outerHeight value."
|
||||
|
||||
log "[UI] kbev.view.outerWidth: " & $kbev.view.outerWidth
|
||||
check kbev.view.outerWidth > 0, "Unexpected view.outerWidth value."
|
||||
|
||||
testRunning[3] = false
|
||||
testCompleted[3] = true
|
||||
log "Test 4 completed."
|
||||
test5()
|
||||
elif testRunning[12]:
|
||||
if kbev.code == "ControlLeft":
|
||||
log "[KB] altKey: " & $kbev.altKey
|
||||
log "[KB] ctrlKey: " & $kbev.ctrlKey
|
||||
log "[KB] metaKey: " & $kbev.metaKey
|
||||
log "[KB] shiftKey: " & $kbev.shiftKey
|
||||
log "[KB] code: " & $kbev.code
|
||||
log "[KB] isComposing: " & $kbev.isComposing
|
||||
log "[KB] key: " & $kbev.key
|
||||
log "[KB] keyCode: " & $kbev.keyCode
|
||||
log "[KB] location: " & $kbev.location
|
||||
var isControl = kbev.getModifierState($KeyboardEventKey.Control)
|
||||
log "[KB] getModifierState result: " & $isControl
|
||||
check isControl, "Unexpected getModifierState return value."
|
||||
completeTest13()
|
||||
|
||||
proc test3() =
|
||||
log "Test 3 started."
|
||||
log "This test validates KeyboardEvent handling."
|
||||
log "Click on the browser to focus, and then press any key."
|
||||
testRunning[2] = true
|
||||
|
||||
proc dragDivDrop(ev: kdom.Event) =
|
||||
dragEventsCompleted[Drop] = true
|
||||
var dgev = cast[DragEvent](ev)
|
||||
var dragDiv = document.getElementById("dragDiv")
|
||||
var data = $dgev.dataTransfer.getData("text/plain")
|
||||
var dt = dgev.dataTransfer
|
||||
|
||||
if not test2ImageDrop:
|
||||
var offset = data.split(",")
|
||||
var newLeft = dgev.clientX + parseInt(offset[0])
|
||||
var newTop = dgev.clientY + parseInt(offset[1])
|
||||
|
||||
if newLeft < 0: newLeft = 0
|
||||
if newTop < 0: newTop = 0
|
||||
if newLeft + dragDiv.clientWidth > window.outerWidth: newLeft = window.outerWidth - dragDiv.clientWidth
|
||||
if newTop + dragDiv.clientHeight > window.outerHeight: newTop = window.outerHeight - dragDiv.clientHeight
|
||||
|
||||
dragDiv.style.left = $newLeft & "px"
|
||||
dragDiv.style.top = $newTop & "px"
|
||||
|
||||
if testRunning[0]:
|
||||
log "[drop] Drop coords: $#, $#" % [$newLeft, $newTop]
|
||||
|
||||
if testCompleted[0] and not testCompleted[1] and not testRunning[1]:
|
||||
testRunning[1] = true
|
||||
log "Test 2 started."
|
||||
log "This test validates the DataTransfer and DataTransferItem objects passed as part of the drag events."
|
||||
var isChrome = ($navigator.userAgent).contains("Chrome")
|
||||
|
||||
log "[DT] effectAllowed: " & $dt.effectAllowed
|
||||
var expectedEffectAllowed = $Uninitialized
|
||||
if isChrome:
|
||||
expectedEffectAllowed = if test2ImageDrop: $DataTransferEffectAllowed.Copy else: $DataTransferEffectAllowed.All
|
||||
check(dt.effectAllowed == $expectedEffectAllowed, "Unexpected effectAllowed value.")
|
||||
|
||||
if not test2ImageDrop:
|
||||
log "[DT] dropEffect: " & $dt.dropEffect
|
||||
var expectedDropEffect = if isChrome: $DataTransferDropEffect.None else: $DataTransferDropEffect.Move
|
||||
check(dt.dropEffect == $expectedDropEffect, "Unexpected dropEffect value.")
|
||||
|
||||
var expectedFilesLen = if test2ImageDrop: 1 else: 0
|
||||
log "[DT] files len: " & $dt.files.len
|
||||
check(dt.files.len == expectedFilesLen, "Unexpected files length.")
|
||||
|
||||
var expectedItemsLen = 1
|
||||
log "[DT] items len: " & $dt.items.len
|
||||
check(dt.items.len == expectedItemsLen, "Unexpected items length.")
|
||||
|
||||
var expectedItemKind = if test2ImageDrop: $DataTransferItemKind.File else: $DataTransferItemKind.String
|
||||
log "[DT] items[0].kind: " & $dt.items[0].kind
|
||||
check(dt.items[0].kind == expectedItemKind, "Unexpected item kind.")
|
||||
|
||||
var expectedItemType = if test2ImageDrop: "image/png" else: "text/plain"
|
||||
log "[DT] items[0].type: " & $dt.items[0].`type`
|
||||
check(dt.items[0].`type` == expectedItemType, "Unexpected item type.")
|
||||
|
||||
var expectedTypesLen = if not isChrome and test2ImageDrop: 2 else: 1
|
||||
log "[DT] types len: " & $dt.types.len
|
||||
check(dt.types.len == expectedTypesLen, "Unexpected types length.")
|
||||
|
||||
if test2ImageDrop:
|
||||
log "[DT] types[0]: " & $dt.types[0]
|
||||
if isChrome:
|
||||
check(dt.types[0] == "Files", "Unexpected type value.")
|
||||
else:
|
||||
log "[DT] types[1]: " & $dt.types[1]
|
||||
check(dt.types[0] == "application/x-moz-file", "Unexpected type value.")
|
||||
check(dt.types[1] == "Files", "Unexpected type value.")
|
||||
|
||||
if test2ImageDrop:
|
||||
test2ImageDrop = false
|
||||
|
||||
# TODO: Determine why getAsString() does invoke callback
|
||||
# proc getAsString*(dti: DataTransferItem, callback: proc(data: cstring))
|
||||
|
||||
var jfile = dt.items[0].getAsFile()
|
||||
log "[DT] file name: " & $jfile.name
|
||||
check(jfile.name.len > 0, "Unexpected file name length.")
|
||||
|
||||
testCompleted[1] = true
|
||||
testRunning[1] = false
|
||||
log "Test 2 completed successfully."
|
||||
test3()
|
||||
|
||||
ev.preventDefault()
|
||||
return
|
||||
|
||||
document.getElementById("dragDiv").style.visibility = "hidden"
|
||||
log "To continue, drag a .PNG file (file type matters) anywhere on this page."
|
||||
log "NOTE: If the browser redirects to the image, then consider this test as failed."
|
||||
test2ImageDrop = true
|
||||
testRunning[1] = false
|
||||
|
||||
ev.preventDefault()
|
||||
|
||||
proc dragDivDrag(ev: kdom.Event) =
|
||||
dragEventsCompleted[Drag] = true
|
||||
ev.preventDefault()
|
||||
|
||||
proc dragDivDragLeave(ev: kdom.Event) =
|
||||
dragEventsCompleted[DragLeave] = true
|
||||
if testRunning[0]:
|
||||
log "[dragLeave]"
|
||||
ev.preventDefault()
|
||||
|
||||
proc dragDivDragExit(ev: kdom.Event) =
|
||||
dragEventsCompleted[DragExit] = true
|
||||
if testRunning[0]:
|
||||
log "[dragExit]"
|
||||
ev.preventDefault()
|
||||
|
||||
proc dragDivDragEnd(ev: kdom.Event) =
|
||||
dragEventsCompleted[DragEnd] = true
|
||||
if testRunning[0]:
|
||||
log "[dragEnd]"
|
||||
ev.preventDefault()
|
||||
|
||||
proc dragDivDragEnter(ev: kdom.Event) =
|
||||
dragEventsCompleted[DragEnter] = true
|
||||
if testRunning[0]:
|
||||
log "[dragEnter]"
|
||||
ev.preventDefault()
|
||||
|
||||
proc initDragEvents() =
|
||||
log "Drag event initialization started."
|
||||
var dragDiv = document.getElementById("dragDiv")
|
||||
document.addEventListener($DragEventTypes.DragOver, dragDivDragOver, false)
|
||||
document.addEventListener($DragEventTypes.Drop, dragDivDrop, false)
|
||||
document.addEventListener($DragEventTypes.Drag, dragDivDrag, false)
|
||||
dragDiv.addEventListener($DragEventTypes.DragStart, dragDivDragStart, false)
|
||||
dragDiv.addEventListener($DragEventTypes.DragExit, dragDivDragExit, false)
|
||||
dragDiv.addEventListener($DragEventTypes.DragLeave, dragDivDragLeave, false)
|
||||
dragDiv.addEventListener($DragEventTypes.DragEnd, dragDivDragEnd, false)
|
||||
dragDiv.addEventListener($DragEventTypes.DragEnter, dragDivDragEnter, false)
|
||||
log "Drag event initialization complete."
|
||||
log "Drag the div to continue the test. Make sure you drag the div over another element."
|
||||
log "Then click the \"Check test 1 status\" button."
|
||||
|
||||
proc initMouseEvents() =
|
||||
log "Mouse event initialization started."
|
||||
document.addEventListener($DomEvent.MouseDown, mouseDown, false)
|
||||
document.addEventListener($DomEvent.MouseMove, mouseMove, false)
|
||||
log "Mouse event initialization complete."
|
||||
|
||||
proc initKbEvents() =
|
||||
log "KB event initialization started."
|
||||
document.addEventListener($DomEvent.KeyDown, keyDown, false)
|
||||
log "KB event initialization complete."
|
||||
|
||||
proc startTests() =
|
||||
log "[Disclaimer: these tests have been tested successfully on Linux with Firefox and Chromium. Your results may vary.]"
|
||||
log "Note: these tests require some manual steps, which will be shown here."
|
||||
log "Note: new entries appear in this text area at the top, so read in this direction."
|
||||
log " | | | | | | | | | | | | | | | | | | | | | |"
|
||||
log " / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\"
|
||||
log "Test 1 started."
|
||||
log "This test checks that drag events are fired."
|
||||
testRunning[0] = true
|
||||
for evt in [Drag, DragEnd, DragEnter, DragExit, DragLeave, DragOver, DragStart, Drop]:
|
||||
dragEventsCompleted.add(evt, false)
|
||||
initKbEvents()
|
||||
initMouseEvents()
|
||||
initDragEvents()
|
||||
|
||||
proc checkTest1Status() =
|
||||
var evtsCompleted = true
|
||||
|
||||
# Chrome doesn't seem to send DragExit, so ignore this
|
||||
if (($navigator.userAgent).contains("Chrome")):
|
||||
dragEventsCompleted[DragExit] = true
|
||||
|
||||
for evt, completed in dragEventsCompleted:
|
||||
if not completed:
|
||||
evtsCompleted = false
|
||||
break
|
||||
|
||||
if evtsCompleted:
|
||||
testRunning[0] = false
|
||||
log "Test 1 completed successfully. Drag the div anywhere to start the next test."
|
||||
testCompleted[0] = true
|
||||
document.getElementById("checkTest1").style.visibility = "hidden"
|
||||
else:
|
||||
log "Test 1 has not completed yet. Make sure you drag the div over another element."
|
||||
log "Status:"
|
||||
for evt, completed in dragEventsCompleted:
|
||||
log $evt & ": " & $completed
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv()):
|
||||
tdiv(id="dragDiv", draggable="true"):
|
||||
tdiv(class="divcontent"):
|
||||
text "draggable div"
|
||||
tdiv(id="results"):
|
||||
for i in countdown(results.len - 1, 0):
|
||||
tdiv:
|
||||
text $results[i]
|
||||
tdiv(id="checkTest1"):
|
||||
button(onclick=checkTest1Status):
|
||||
text "Check test 1 status"
|
||||
input(`type`="hidden", id="test9value")
|
||||
input(`type`="text", id="test10input1")
|
||||
input(`type`="text", id="test10input2")
|
||||
h3(id="test11h3"):
|
||||
text "test 11"
|
||||
input(`type`="text", id="test12input")
|
||||
|
||||
proc onload() =
|
||||
startTests()
|
||||
|
||||
setRenderer createDom
|
||||
onload()
|
||||
10253
tests/jquery-3.2.1.js
vendored
Normal file
10253
tests/jquery-3.2.1.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
18
tests/lists.nim
Normal file
18
tests/lists.nim
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
|
||||
|
||||
var contents: seq[cstring] = @[]
|
||||
|
||||
proc onTodoEnter(e: Event; n: VNode) =
|
||||
contents.insert(n.value & "BBBB")
|
||||
contents.insert(n.value)
|
||||
n.value = ""
|
||||
|
||||
proc createDom(): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
|
||||
onkeyupenter = onTodoEnter, setFocus)
|
||||
for c in contents:
|
||||
tdiv:
|
||||
text c
|
||||
|
||||
setRenderer createDom
|
||||
27
tests/nativehtmlgen.nim
Normal file
27
tests/nativehtmlgen.nim
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## Simple test that shows Karax can also do client-side HTML rendering.
|
||||
|
||||
import "../karax" / [karaxdsl, vdom]
|
||||
|
||||
when defined(js):
|
||||
{.error: "Use 'nim c' to compile this example".}
|
||||
|
||||
template kxi(): int = 0
|
||||
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
|
||||
n.setAttr($k, action)
|
||||
|
||||
let tab = buildHtml(table):
|
||||
tr:
|
||||
td:
|
||||
text "Cell A"
|
||||
td:
|
||||
text "Cell B"
|
||||
tr:
|
||||
td:
|
||||
text "Cell C"
|
||||
td:
|
||||
text "Cell D"
|
||||
td:
|
||||
a(href = "#/", onclick = "javascript:myFunc()"):
|
||||
text"haha"
|
||||
|
||||
echo tab
|
||||
1
tests/nim.cfg
Normal file
1
tests/nim.cfg
Normal file
|
|
@ -0,0 +1 @@
|
|||
--path: "../karax"
|
||||
22
tests/tester.nim
Normal file
22
tests/tester.nim
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# For now we only test that the things still compile.
|
||||
|
||||
import os
|
||||
|
||||
proc exec(cmd: string) =
|
||||
if os.execShellCmd(cmd) != 0:
|
||||
quit "command failed " & cmd
|
||||
|
||||
proc main =
|
||||
exec("nim js tests/diffDomTests.nim")
|
||||
exec("nim js tests/compiler_tests.nim")
|
||||
exec("nim js examples/todoapp/todoapp.nim")
|
||||
exec("nim js examples/scrollapp/scrollapp.nim")
|
||||
exec("nim js examples/mediaplayer/playerapp.nim")
|
||||
exec("nim js examples/carousel/carousel.nim")
|
||||
exec("nim js -d:nodejs -r tests/difftest.nim")
|
||||
exec("nim c tests/nativehtmlgen.nim")
|
||||
|
||||
for test in os.walkFiles("examples/*.nim"):
|
||||
exec("nim js " & test)
|
||||
|
||||
main()
|
||||
6
todo.rst
6
todo.rst
|
|
@ -1,6 +0,0 @@
|
|||
- Better virtual dom diff'ing algorithm: find longest common subsequence
|
||||
- add CSS changes to todo example.nim
|
||||
- handle onscroll events
|
||||
- introduce a way to test karax applications
|
||||
- introduce a way to do AJAX
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue