Compare commits
142 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 |
78 changed files with 14873 additions and 652 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
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,2 +1,7 @@
|
||||||
nimcache/
|
nimcache/
|
||||||
/tests/tester
|
/tests/tester
|
||||||
|
karax/tools/karun
|
||||||
|
*.code-workspace
|
||||||
|
*.exe
|
||||||
|
app.js
|
||||||
|
app.html
|
||||||
|
|
|
||||||
18
.travis.yml
18
.travis.yml
|
|
@ -1,18 +0,0 @@
|
||||||
sudo: false
|
|
||||||
language: c
|
|
||||||
os: linux
|
|
||||||
install:
|
|
||||||
- git clone https://github.com/nim-lang/nim
|
|
||||||
- cd nim
|
|
||||||
- git clone --depth 1 https://github.com/nim-lang/csources.git
|
|
||||||
- cd csources
|
|
||||||
- sh build.sh
|
|
||||||
- cd ../
|
|
||||||
- bin/nim c koch
|
|
||||||
- ./koch boot -d:release
|
|
||||||
- cd ..
|
|
||||||
before_script:
|
|
||||||
- set -e
|
|
||||||
- export PATH=$(pwd)/nim/bin:$(pwd):$PATH
|
|
||||||
script:
|
|
||||||
- nim c -r tests/tester.nim
|
|
||||||
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.
|
||||||
|
|
@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system.
|
||||||
These extensions are:
|
These extensions are:
|
||||||
|
|
||||||
``VNodeKind.int``
|
``VNodeKind.int``
|
||||||
The node havs a single integer field.
|
The node has a single integer field.
|
||||||
|
|
||||||
``VNodeKind.bool``
|
``VNodeKind.bool``
|
||||||
The node has a single boolean field.
|
The node has a single boolean field.
|
||||||
|
|
||||||
``VNodeKint.vthunk``
|
``VNodeKind.vthunk``
|
||||||
The node is a `virtual thunk`:idx:. This means there is a
|
The node is a `virtual thunk`:idx:. This means there is a
|
||||||
function attached to it that produces the ``VNode`` structure
|
function attached to it that produces the ``VNode`` structure
|
||||||
on demand.
|
on demand.
|
||||||
|
|
||||||
``VNodeKint.dthunk``
|
``VNodeKind.dthunk``
|
||||||
The node is a `DOM thunk`:idx:. This means there is a
|
The node is a `DOM thunk`:idx:. This means there is a
|
||||||
function attached to it that produces the ``Node`` DOM structure
|
function attached to it that produces the ``Node`` DOM structure
|
||||||
on demand.
|
on demand.
|
||||||
|
|
@ -53,6 +53,6 @@ Event system
|
||||||
|
|
||||||
Karax does not abstract over the event system the DOM offers much: The same
|
Karax does not abstract over the event system the DOM offers much: The same
|
||||||
``Event`` structure is used. Every callback has the
|
``Event`` structure is used. Every callback has the
|
||||||
signature ``proc (ev: Event; n: VNode)``.
|
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
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/carousel.js"></script>
|
<script type="text/javascript" src="carousel.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
--path: "../../src"
|
--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
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import karax, karaxdsl, vdom, kdom, compact
|
import karax / [karax, karaxdsl, vdom, kdom, compact]
|
||||||
|
|
||||||
const
|
const
|
||||||
Play = 0
|
Play = 0
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/playerapp.js"></script>
|
<script type="text/javascript" src="playerapp.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
## Example app that shows how to write and embed a custom component.
|
## Example app that shows how to write and embed a custom component.
|
||||||
|
|
||||||
include karaxprelude
|
include karax/prelude
|
||||||
import mediaplayer
|
import mediaplayer
|
||||||
|
|
||||||
const url = "https://www.w3schools.com/html/mov_bbb.mp4"
|
const url = "https://www.w3schools.com/html/mov_bbb.mp4"
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
--path: "../../src"
|
--path: "../.."
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
|
<script type="text/javascript" src="scrollapp.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
## Example that shows how to accomplish an "infinitely scrolling" app.
|
## Example that shows how to accomplish an "infinitely scrolling" app.
|
||||||
|
|
||||||
include karaxprelude
|
include karax/prelude
|
||||||
import jstrutils, kdom, vstyles
|
import karax / [jstrutils, kdom, vstyles]
|
||||||
|
|
||||||
var entries: seq[cstring] = @[]
|
var entries: seq[cstring] = @[]
|
||||||
for i in 1..500:
|
for i in 1..500:
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
--path: "../../src"
|
--path: "../.."
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/todoapp.js"></script>
|
<script type="text/javascript" src="todoapp.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import vdom, karax, karaxdsl, jstrutils, compact, localstorage
|
import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
|
||||||
|
|
||||||
type
|
type
|
||||||
Filter = enum
|
Filter = enum
|
||||||
|
|
@ -9,7 +9,7 @@ var
|
||||||
selectedEntry = -1
|
selectedEntry = -1
|
||||||
filter: Filter
|
filter: Filter
|
||||||
entriesLen: int
|
entriesLen: int
|
||||||
|
doneswitch = true
|
||||||
const
|
const
|
||||||
contentSuffix = cstring"content"
|
contentSuffix = cstring"content"
|
||||||
completedSuffix = cstring"completed"
|
completedSuffix = cstring"completed"
|
||||||
|
|
@ -41,6 +41,7 @@ proc updateEntry(pos: int, content: cstring, completed: bool) =
|
||||||
markAsCompleted(pos, completed)
|
markAsCompleted(pos, completed)
|
||||||
|
|
||||||
proc onTodoEnter(ev: Event; n: VNode) =
|
proc onTodoEnter(ev: Event; n: VNode) =
|
||||||
|
if n.value.strip() != "":
|
||||||
addEntry(n.value, false)
|
addEntry(n.value, false)
|
||||||
n.value = ""
|
n.value = ""
|
||||||
|
|
||||||
|
|
@ -61,9 +62,9 @@ proc toggleEntry(ev: Event; n: VNode) =
|
||||||
markAsCompleted(id, not isCompleted(id))
|
markAsCompleted(id, not isCompleted(id))
|
||||||
|
|
||||||
proc onAllDone(ev: Event; n: VNode) =
|
proc onAllDone(ev: Event; n: VNode) =
|
||||||
clear()
|
for i in 0..<entriesLen:
|
||||||
selectedEntry = -1
|
markAsCompleted(i, doneswitch)
|
||||||
|
doneswitch = not doneswitch
|
||||||
proc clearCompleted(ev: Event, n: VNode) =
|
proc clearCompleted(ev: Event, n: VNode) =
|
||||||
for i in 0..<entriesLen:
|
for i in 0..<entriesLen:
|
||||||
if isCompleted(i): setEntryContent(i, nil)
|
if isCompleted(i): setEntryContent(i, nil)
|
||||||
|
|
@ -71,9 +72,6 @@ proc clearCompleted(ev: Event, n: VNode) =
|
||||||
proc toClass(completed: bool): cstring =
|
proc toClass(completed: bool): cstring =
|
||||||
(if completed: cstring"completed" else: cstring(nil))
|
(if completed: cstring"completed" else: cstring(nil))
|
||||||
|
|
||||||
proc toChecked(checked: bool): cstring =
|
|
||||||
(if checked: cstring"checked" else: cstring(nil))
|
|
||||||
|
|
||||||
proc selected(v: Filter): cstring =
|
proc selected(v: Filter): cstring =
|
||||||
(if filter == v: cstring"selected" else: cstring(nil))
|
(if filter == v: cstring"selected" else: cstring(nil))
|
||||||
|
|
||||||
|
|
@ -92,7 +90,7 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compac
|
||||||
onblur = focusLost,
|
onblur = focusLost,
|
||||||
onkeyupenter = editEntry, value = d, setFocus=true)
|
onkeyupenter = editEntry, value = d, setFocus=true)
|
||||||
|
|
||||||
proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} =
|
proc makeFooter(entriesCount, completedCount: int): VNode =
|
||||||
result = buildHtml(footer(class = "footer")):
|
result = buildHtml(footer(class = "footer")):
|
||||||
span(class = "todo-count"):
|
span(class = "todo-count"):
|
||||||
strong:
|
strong:
|
||||||
|
|
@ -118,13 +116,16 @@ proc makeHeader(): VNode {.compact.} =
|
||||||
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
|
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
|
||||||
onkeyupenter = onTodoEnter, setFocus)
|
onkeyupenter = onTodoEnter, setFocus)
|
||||||
|
|
||||||
proc createDom(): VNode =
|
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")):
|
result = buildHtml(tdiv(class="todomvc-wrapper")):
|
||||||
section(class = "todoapp"):
|
section(class = "todoapp"):
|
||||||
makeHeader()
|
makeHeader()
|
||||||
section(class = "main"):
|
section(class = "main"):
|
||||||
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
|
input(class = "toggle-all", `type` = "checkbox", id = "toggle", onclick = onAllDone)
|
||||||
label(`for` = "toggle-all", onclick = onAllDone):
|
label(`for` = "toggle"):
|
||||||
text "Mark all as complete"
|
text "Mark all as complete"
|
||||||
var entriesCount = 0
|
var entriesCount = 0
|
||||||
var completedCount = 0
|
var completedCount = 0
|
||||||
|
|
@ -144,12 +145,6 @@ proc createDom(): VNode =
|
||||||
inc entriesCount
|
inc entriesCount
|
||||||
makeFooter(entriesCount, completedCount)
|
makeFooter(entriesCount, completedCount)
|
||||||
|
|
||||||
setOnHashChange(proc(hash: cstring) =
|
|
||||||
if hash == "#/": filter = all
|
|
||||||
elif hash == "#/completed": filter = completed
|
|
||||||
elif hash == "#/active": filter = active
|
|
||||||
)
|
|
||||||
|
|
||||||
if hasItem(lenSuffix):
|
if hasItem(lenSuffix):
|
||||||
entriesLen = parseInt getItem(lenSuffix)
|
entriesLen = parseInt getItem(lenSuffix)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -9,5 +9,5 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/ajaxtest.js"></script>
|
<script type="text/javascript" src="ajaxtest.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
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"
|
||||||
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
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
|
|
||||||
<body id="body">
|
<body id="body">
|
||||||
<div id="ROOT"></div>
|
<div id="ROOT"></div>
|
||||||
<script type="text/javascript" src="nimcache/demo.js"></script>
|
<script type="text/javascript" src="demo.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
|
<script type="text/javascript" src="scrollapp.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
onload(window.location.pathname.split('/')[1])
|
onload(window.location.pathname.split('/')[1])
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
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
|
||||||
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
|
||||||
13
karax.nimble
13
karax.nimble
|
|
@ -1,13 +1,16 @@
|
||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.1.0"
|
version = "1.2.1"
|
||||||
author = "Xored Software, Inc."
|
author = "Andreas Rumpf"
|
||||||
description = "Karax is a framework for developing single page applications in Nim."
|
description = "Karax is a framework for developing single page applications in Nim."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
srcDir = "src"
|
|
||||||
|
|
||||||
# Dependencies
|
# 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, "")
|
||||||
|
|
@ -11,6 +11,12 @@ proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(
|
||||||
|
|
||||||
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
|
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
|
type
|
||||||
JSeq*[T] = ref object
|
JSeq*[T] = ref object
|
||||||
|
|
||||||
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 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 startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
|
||||||
proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", 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: int): cstring {.importcpp: "substr", nodecl.}
|
||||||
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
|
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", 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".}
|
||||||
|
|
||||||
|
|
@ -2,18 +2,21 @@
|
||||||
|
|
||||||
import kdom, vdom, jstrutils, compact, jdict, vstyles
|
import kdom, vdom, jstrutils, compact, jdict, vstyles
|
||||||
|
|
||||||
export kdom.Event
|
export kdom.Event, kdom.Blob
|
||||||
|
|
||||||
|
when defined(nimNoNil):
|
||||||
|
{.experimental: "notnil".}
|
||||||
|
|
||||||
proc kout*[T](x: T) {.importc: "console.log", varargs.}
|
proc kout*[T](x: T) {.importc: "console.log", varargs.}
|
||||||
## the preferred way of debugging karax applications.
|
## The preferred way of debugging karax applications.
|
||||||
|
|
||||||
type
|
type
|
||||||
PatchKind = enum
|
PatchKind = enum
|
||||||
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach
|
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
|
||||||
Patch = object
|
Patch = object
|
||||||
k: PatchKind
|
k: PatchKind
|
||||||
parent, current: Node
|
parent, current: Node
|
||||||
n: VNode
|
newNode, oldNode: VNode
|
||||||
PatchV = object
|
PatchV = object
|
||||||
parent, newChild: VNode
|
parent, newChild: VNode
|
||||||
pos: int
|
pos: int
|
||||||
|
|
@ -22,15 +25,19 @@ type
|
||||||
parent, current: Node
|
parent, current: Node
|
||||||
|
|
||||||
type
|
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
|
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
|
||||||
## know about this.
|
## know about this.
|
||||||
rootId: cstring not nil
|
rootId: cstring
|
||||||
renderer: proc (): VNode {.closure.}
|
renderer: proc (data: RouterData): VNode {.closure.}
|
||||||
currentTree: VNode
|
currentTree: VNode
|
||||||
postRenderCallback: proc ()
|
postRenderCallback: proc (data: RouterData)
|
||||||
toFocus: Node
|
toFocus: Node
|
||||||
toFocusV: VNode
|
toFocusV: VNode
|
||||||
renderId: int
|
renderId: int
|
||||||
|
rendering: bool
|
||||||
patches: seq[Patch] # we reuse this to save allocations
|
patches: seq[Patch] # we reuse this to save allocations
|
||||||
patchLen: int
|
patchLen: int
|
||||||
patchesV: seq[PatchV]
|
patchesV: seq[PatchV]
|
||||||
|
|
@ -38,8 +45,10 @@ type
|
||||||
runCount: int
|
runCount: int
|
||||||
components: seq[ComponentPair]
|
components: seq[ComponentPair]
|
||||||
surpressRedraws*: bool
|
surpressRedraws*: bool
|
||||||
|
byId: JDict[cstring, VNode]
|
||||||
when defined(stats):
|
when defined(stats):
|
||||||
recursion: int
|
recursion: int
|
||||||
|
orphans: JDict[cstring, bool]
|
||||||
|
|
||||||
|
|
||||||
var
|
var
|
||||||
|
|
@ -69,6 +78,24 @@ template keyeventBody() =
|
||||||
# Do not call redraw() here! That is already done
|
# Do not call redraw() here! That is already done
|
||||||
# by ``karax.addEventHandler``.
|
# 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;
|
proc wrapEvent(d: Node; n: VNode; k: EventKind;
|
||||||
action: EventHandler): NativeEventHandler =
|
action: EventHandler): NativeEventHandler =
|
||||||
proc stdWrapper(): NativeEventHandler =
|
proc stdWrapper(): NativeEventHandler =
|
||||||
|
|
@ -83,7 +110,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
|
||||||
let action = action
|
let action = action
|
||||||
let n = n
|
let n = n
|
||||||
result = proc (ev: Event) =
|
result = proc (ev: Event) =
|
||||||
if ev.keyCode == 13: keyeventBody()
|
if cast[KeyboardEvent](ev).keyCode == 13: keyeventBody()
|
||||||
|
|
||||||
proc laterWrapper(): NativeEventHandler =
|
proc laterWrapper(): NativeEventHandler =
|
||||||
let action = action
|
let action = action
|
||||||
|
|
@ -97,59 +124,75 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
|
||||||
case k
|
case k
|
||||||
of EventKind.onkeyuplater:
|
of EventKind.onkeyuplater:
|
||||||
result = laterWrapper()
|
result = laterWrapper()
|
||||||
d.addEventListener("keyup", result)
|
d.addEventShell("keyup", result)
|
||||||
of EventKind.onkeyupenter:
|
of EventKind.onkeyupenter:
|
||||||
result = enterWrapper()
|
result = enterWrapper()
|
||||||
d.addEventListener("keyup", result)
|
d.addEventShell("keyup", result)
|
||||||
else:
|
else:
|
||||||
result = stdWrapper()
|
result = stdWrapper()
|
||||||
d.addEventListener(toEventName[k], result)
|
d.addEventShell(toEventName[k], result)
|
||||||
|
|
||||||
# --------------------- DOM diff -----------------------------------------
|
# --------------------- DOM diff -----------------------------------------
|
||||||
|
|
||||||
template detach(n: VNode) =
|
template detach(n: VNode) =
|
||||||
addPatch(kxi, pkDetach, nil, nil, n)
|
addPatch(kxi, pkDetach, nil, nil, nil, n)
|
||||||
|
|
||||||
template attach(n: VNode) =
|
template attach(n: VNode) =
|
||||||
n.dom = result
|
n.dom = result
|
||||||
|
if n.id != nil: kxi.byId[n.id] = n
|
||||||
|
|
||||||
proc applyEvents(n: VNode; kxi: KaraxInstance) =
|
proc applyEvents(n: VNode) =
|
||||||
let dest = n.dom
|
let dest = n.dom
|
||||||
for i in 0..<len(n.events):
|
for i in 0..<len(n.events):
|
||||||
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
|
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
|
||||||
|
|
||||||
proc vnodeToDom*(n: VNode; kxi: KaraxInstance): Node =
|
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:
|
if n.kind == VNodeKind.text:
|
||||||
result = document.createTextNode(n.text)
|
result = document.createTextNode(n.text)
|
||||||
attach n
|
attach n
|
||||||
|
elif n.kind == VNodeKind.verbatim:
|
||||||
|
result = document.createElement("div")
|
||||||
|
result.innerHTML = n.text
|
||||||
|
attach n
|
||||||
|
return result
|
||||||
elif n.kind == VNodeKind.vthunk:
|
elif n.kind == VNodeKind.vthunk:
|
||||||
let x = callThunk(vcomponents[n.text], n)
|
let x = callThunk(vcomponents[n.text], n)
|
||||||
result = vnodeToDom(x, kxi)
|
result = toDom(x, useAttachedNode, kxi)
|
||||||
#n.key = result.key
|
#n.key = result.key
|
||||||
attach n
|
attach n
|
||||||
return result
|
return result
|
||||||
elif n.kind == VNodeKind.dthunk:
|
elif n.kind == VNodeKind.dthunk:
|
||||||
result = callThunk(dcomponents[n.text], n)
|
result = n.dom #callThunk(dcomponents[n.text], n)
|
||||||
|
assert result != nil
|
||||||
#n.key = result.key
|
#n.key = result.key
|
||||||
attach n
|
attach n
|
||||||
return result
|
return result
|
||||||
elif n.kind == VNodeKind.component:
|
elif n.kind == VNodeKind.component:
|
||||||
let x = VComponent(n)
|
let x = VComponent(n)
|
||||||
if x.realDomImpl != nil: return x.realDomImpl(x)
|
|
||||||
if x.onAttachImpl != nil: x.onAttachImpl(x)
|
if x.onAttachImpl != nil: x.onAttachImpl(x)
|
||||||
assert x.renderImpl != nil
|
assert x.renderImpl != nil
|
||||||
if x.expanded == nil:
|
if x.expanded == nil:
|
||||||
x.expanded = x.renderImpl(x)
|
x.expanded = x.renderImpl(x)
|
||||||
# x.updatedImpl(x, nil)
|
# x.updatedImpl(x, nil)
|
||||||
assert x.expanded != nil
|
assert x.expanded != nil
|
||||||
result = vnodeToDom(x.expanded, kxi)
|
result = toDom(x.expanded, useAttachedNode, kxi)
|
||||||
attach n
|
attach n
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
result = document.createElement(toTag[n.kind])
|
result = document.createElement(toTag[n.kind])
|
||||||
attach n
|
attach n
|
||||||
for k in n:
|
for k in n:
|
||||||
appendChild(result, vnodeToDom(k, kxi))
|
appendChild(result, toDom(k, useAttachedNode, kxi))
|
||||||
# text is mapped to 'value':
|
# text is mapped to 'value':
|
||||||
if n.text != nil:
|
if n.text != nil:
|
||||||
result.value = n.text
|
result.value = n.text
|
||||||
|
|
@ -162,22 +205,26 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance): Node =
|
||||||
for k, v in attrs(n):
|
for k, v in attrs(n):
|
||||||
if v != nil:
|
if v != nil:
|
||||||
result.setAttr(k, v)
|
result.setAttr(k, v)
|
||||||
applyEvents(n, kxi)
|
applyEvents(n)
|
||||||
if n == kxi.toFocusV and kxi.toFocus.isNil:
|
if kxi != nil and n == kxi.toFocusV and kxi.toFocus.isNil:
|
||||||
kxi.toFocus = result
|
kxi.toFocus = result
|
||||||
if not n.style.isNil: applyStyle(result, n.style)
|
if not n.style.isNil: applyStyle(result, n.style)
|
||||||
|
|
||||||
proc same(n: VNode, e: Node; nesting = 0): bool =
|
proc same(n: VNode, e: Node; nesting = 0): bool =
|
||||||
|
if kxi.orphans.contains(n.id): return true
|
||||||
if n.kind == VNodeKind.component:
|
if n.kind == VNodeKind.component:
|
||||||
result = same(VComponent(n).expanded, e, nesting+1)
|
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:
|
elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk:
|
||||||
# we don't check these for now:
|
# we don't check these:
|
||||||
result = true
|
result = true
|
||||||
elif toTag[n.kind] == e.nodename:
|
elif toTag[n.kind] == e.nodename:
|
||||||
result = true
|
result = true
|
||||||
if n.kind != VNodeKind.text:
|
if n.kind != VNodeKind.text:
|
||||||
if e.len != n.len:
|
# BUGFIX: Microsoft's Edge gives the textarea a child containing the text node!
|
||||||
echo "expected ", e.len, " real ", n.len, toTag[n.kind], " nesting ", nesting
|
if e.len != n.len and n.kind != VNodeKind.textarea:
|
||||||
|
echo "expected ", n.len, " real ", e.len, " ", toTag[n.kind], " nesting ", nesting
|
||||||
return false
|
return false
|
||||||
for i in 0 ..< n.len:
|
for i in 0 ..< n.len:
|
||||||
if not same(n[i], e[i], nesting+1): return false
|
if not same(n[i], e[i], nesting+1): return false
|
||||||
|
|
@ -206,7 +253,7 @@ when defined(profileKarax):
|
||||||
for i in low(DifferEnum)..high(DifferEnum):
|
for i in low(DifferEnum)..high(DifferEnum):
|
||||||
echo i, " value: ", a[i]
|
echo i, " value: ", a[i]
|
||||||
|
|
||||||
proc eq(a, b: VNode): EqResult =
|
proc eq(a, b: VNode; recursive: bool): EqResult =
|
||||||
if a.kind != b.kind:
|
if a.kind != b.kind:
|
||||||
when defined(profileKarax): inc reasons[deKind]
|
when defined(profileKarax): inc reasons[deKind]
|
||||||
return different
|
return different
|
||||||
|
|
@ -220,12 +267,20 @@ proc eq(a, b: VNode): EqResult =
|
||||||
if a.kind == VNodeKind.text:
|
if a.kind == VNodeKind.text:
|
||||||
if a.text != b.text:
|
if a.text != b.text:
|
||||||
when defined(profileKarax): inc reasons[deText]
|
when defined(profileKarax): inc reasons[deText]
|
||||||
return similar
|
return different # similar
|
||||||
elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk:
|
elif a.kind == VNodeKind.vthunk:
|
||||||
if a.text != b.text: return different
|
if a.text != b.text: return different
|
||||||
if a.len != b.len: return different
|
if a.len != b.len: return different
|
||||||
for i in 0..<a.len:
|
for i in 0..<a.len:
|
||||||
if eq(a[i], b[i]) == different: return different
|
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:
|
elif b.kind == VNodeKind.component:
|
||||||
# different component names mean different components:
|
# different component names mean different components:
|
||||||
if a.text != b.text:
|
if a.text != b.text:
|
||||||
|
|
@ -238,12 +293,20 @@ proc eq(a, b: VNode): EqResult =
|
||||||
when defined(profileKarax): inc reasons[deComponent]
|
when defined(profileKarax): inc reasons[deComponent]
|
||||||
return different
|
return different
|
||||||
return componentsIdentical
|
return componentsIdentical
|
||||||
if a.class != b.class:
|
#if:
|
||||||
when defined(profileKarax): inc reasons[deClass]
|
# when defined(profileKarax): inc reasons[deClass]
|
||||||
return different
|
# return different
|
||||||
if not eq(a.style, b.style) or not sameAttrs(a, b):
|
if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b):
|
||||||
when defined(profileKarax): inc reasons[deSimilar]
|
when defined(profileKarax): inc reasons[deSimilar]
|
||||||
return similar
|
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!
|
# Do not test event listeners here!
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -252,7 +315,9 @@ proc updateStyles(newNode, oldNode: VNode) =
|
||||||
if oldNode.dom != nil:
|
if oldNode.dom != nil:
|
||||||
if newNode.style != nil: applyStyle(oldNode.dom, newNode.style)
|
if newNode.style != nil: applyStyle(oldNode.dom, newNode.style)
|
||||||
else: oldNode.dom.style = Style()
|
else: oldNode.dom.style = Style()
|
||||||
|
oldNode.dom.class = newNode.class
|
||||||
oldNode.style = newNode.style
|
oldNode.style = newNode.style
|
||||||
|
oldNode.class = newNode.class
|
||||||
|
|
||||||
proc updateAttributes(newNode, oldNode: VNode) =
|
proc updateAttributes(newNode, oldNode: VNode) =
|
||||||
# we keep the oldNode, but take over the attributes from the new node:
|
# we keep the oldNode, but take over the attributes from the new node:
|
||||||
|
|
@ -266,6 +331,9 @@ proc updateAttributes(newNode, oldNode: VNode) =
|
||||||
|
|
||||||
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
|
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
|
||||||
let d = oldNode.dom
|
let d = oldNode.dom
|
||||||
|
if d != nil:
|
||||||
|
removeAllEventHandlers(d)
|
||||||
|
when false:
|
||||||
for i in 0..<oldNode.events.len:
|
for i in 0..<oldNode.events.len:
|
||||||
let k = oldNode.events[i][0]
|
let k = oldNode.events[i][0]
|
||||||
let name = case k
|
let name = case k
|
||||||
|
|
@ -273,8 +341,9 @@ proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
|
||||||
else: toEventName[k]
|
else: toEventName[k]
|
||||||
d.removeEventListener(name, oldNode.events[i][2])
|
d.removeEventListener(name, oldNode.events[i][2])
|
||||||
shallowCopy(oldNode.events, newNode.events)
|
shallowCopy(oldNode.events, newNode.events)
|
||||||
applyEvents(oldNode, kxi)
|
applyEvents(oldNode)
|
||||||
|
|
||||||
|
when false:
|
||||||
proc printV(n: VNode; depth: cstring = "") =
|
proc printV(n: VNode; depth: cstring = "") =
|
||||||
kout depth, cstring($n.kind), cstring"key ", n.index
|
kout depth, cstring($n.kind), cstring"key ", n.index
|
||||||
#for k, v in pairs(n.style):
|
#for k, v in pairs(n.style):
|
||||||
|
|
@ -288,16 +357,18 @@ proc printV(n: VNode; depth: cstring = "") =
|
||||||
printV(n[i], depth & " ")
|
printV(n[i], depth & " ")
|
||||||
|
|
||||||
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
|
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
|
||||||
na: VNode) =
|
na, oldNode: VNode) =
|
||||||
let L = kxi.patchLen
|
let L = kxi.patchLen
|
||||||
if L >= kxi.patches.len:
|
if L >= kxi.patches.len:
|
||||||
# allocate more space:
|
# allocate more space:
|
||||||
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na))
|
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
|
||||||
|
newNode: na, oldNode: oldNode))
|
||||||
else:
|
else:
|
||||||
kxi.patches[L].k = ka
|
kxi.patches[L].k = ka
|
||||||
kxi.patches[L].parent = parenta
|
kxi.patches[L].parent = parenta
|
||||||
kxi.patches[L].current = currenta
|
kxi.patches[L].current = currenta
|
||||||
kxi.patches[L].n = na
|
kxi.patches[L].newNode = na
|
||||||
|
kxi.patches[L].oldNode = oldNode
|
||||||
inc kxi.patchLen
|
inc kxi.patchLen
|
||||||
|
|
||||||
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
|
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
|
||||||
|
|
@ -311,26 +382,41 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
|
||||||
kxi.patchesV[L].pos = pos
|
kxi.patchesV[L].pos = pos
|
||||||
inc kxi.patchLenV
|
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) =
|
proc applyPatch(kxi: KaraxInstance) =
|
||||||
for i in 0..<kxi.patchLen:
|
for i in 0..<kxi.patchLen:
|
||||||
let p = kxi.patches[i]
|
let p = kxi.patches[i]
|
||||||
case p.k
|
case p.k
|
||||||
of pkReplace:
|
of pkReplace:
|
||||||
let nn = vnodeToDom(p.n, kxi)
|
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||||
if p.parent == nil:
|
if p.parent == nil:
|
||||||
replaceById(kxi.rootId, nn)
|
replaceById(kxi.rootId, nn)
|
||||||
else:
|
else:
|
||||||
|
if p.current.parentNode == p.parent:
|
||||||
p.parent.replaceChild(nn, p.current)
|
p.parent.replaceChild(nn, p.current)
|
||||||
|
else: # fix #121
|
||||||
|
p.parent.appendChild(nn)
|
||||||
|
of pkSame:
|
||||||
|
moveDom(p.newNode, p.oldNode)
|
||||||
of pkRemove:
|
of pkRemove:
|
||||||
p.parent.removeChild(p.current)
|
p.parent.removeChild(p.current)
|
||||||
of pkAppend:
|
of pkAppend:
|
||||||
let nn = vnodeToDom(p.n, kxi)
|
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||||
p.parent.appendChild(nn)
|
p.parent.appendChild(nn)
|
||||||
of pkInsertBefore:
|
of pkInsertBefore:
|
||||||
let nn = vnodeToDom(p.n, kxi)
|
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||||
p.parent.insertBefore(nn, p.current)
|
p.parent.insertBefore(nn, p.current)
|
||||||
of pkDetach:
|
of pkDetach:
|
||||||
let n = p.n
|
let n = p.oldNode
|
||||||
|
if n.id != nil: kxi.byId.del(n.id)
|
||||||
if n.kind == VNodeKind.component:
|
if n.kind == VNodeKind.component:
|
||||||
let x = VComponent(n)
|
let x = VComponent(n)
|
||||||
if x.onDetachImpl != nil: x.onDetachImpl(x)
|
if x.onDetachImpl != nil: x.onDetachImpl(x)
|
||||||
|
|
@ -343,16 +429,38 @@ proc applyPatch(kxi: KaraxInstance) =
|
||||||
assert p.newChild.dom != nil
|
assert p.newChild.dom != nil
|
||||||
kxi.patchLenV = 0
|
kxi.patchLenV = 0
|
||||||
|
|
||||||
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult =
|
# 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):
|
when defined(stats):
|
||||||
if kxi.recursion > 100:
|
if kxi.recursion > 100:
|
||||||
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode)
|
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
|
||||||
if oldNode.kind == VNodeKind.text:
|
if oldNode.kind == VNodeKind.text:
|
||||||
echo oldNode.text
|
echo oldNode.text
|
||||||
#return
|
|
||||||
#doAssert false, "overflow!"
|
|
||||||
inc kxi.recursion
|
inc kxi.recursion
|
||||||
result = eq(newNode, oldNode)
|
let result = eq(newNode, oldNode, false)
|
||||||
case result
|
case result
|
||||||
of componentsIdentical:
|
of componentsIdentical:
|
||||||
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
|
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
|
||||||
|
|
@ -370,90 +478,54 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E
|
||||||
|
|
||||||
if newNode.events.len != 0 or oldNode.events.len != 0:
|
if newNode.events.len != 0 or oldNode.events.len != 0:
|
||||||
mergeEvents(newNode, oldNode, kxi)
|
mergeEvents(newNode, oldNode, kxi)
|
||||||
when false:
|
|
||||||
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
|
|
||||||
if oldNode.text != newNode.text:
|
|
||||||
oldNode.text = newNode.text
|
|
||||||
oldNode.dom.value = newNode.text
|
|
||||||
|
|
||||||
let newLength = newNode.len
|
let newLength = newNode.len
|
||||||
let oldLength = oldNode.len
|
let oldLength = oldNode.len
|
||||||
if newLength == 0 and oldLength == 0: return result
|
if newLength == 0 and oldLength == 0: return
|
||||||
let minLength = min(newLength, oldLength)
|
let minLength = min(newLength, oldLength)
|
||||||
|
|
||||||
assert oldNode.kind == newNode.kind
|
assert oldNode.kind == newNode.kind
|
||||||
var commonPrefix = 0
|
var commonPrefix = 0
|
||||||
let isSpecial = oldNode.kind == VNodeKind.component or
|
|
||||||
oldNode.kind == VNodeKind.vthunk or
|
|
||||||
oldNode.kind == VNodeKind.dthunk
|
|
||||||
|
|
||||||
template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) =
|
|
||||||
let oldLen = kxi.patchLen
|
|
||||||
let oldLenV = kxi.patchLenV
|
|
||||||
assert i < a.len
|
|
||||||
assert j < b.len
|
|
||||||
let r = if isSpecial:
|
|
||||||
diff(a[i], b[j], parent, current, kxi)
|
|
||||||
else:
|
|
||||||
diff(a[i], b[j], current, current.childNodes[j], kxi)
|
|
||||||
case r
|
|
||||||
of identical, componentsIdentical, similar:
|
|
||||||
a[i] = b[j]
|
|
||||||
action
|
|
||||||
of usenewNode:
|
|
||||||
kxi.addPatchV(b, j, a[i])
|
|
||||||
action
|
|
||||||
# unfortunately, we need to propagate the changes upwards:
|
|
||||||
result = useNewNode
|
|
||||||
of different:
|
|
||||||
# undo what 'diff' would have done:
|
|
||||||
kxi.patchLen = oldLen
|
|
||||||
kxi.patchLenV = oldLenV
|
|
||||||
if result != different: result = r
|
|
||||||
break
|
|
||||||
# compute common prefix:
|
# compute common prefix:
|
||||||
while commonPrefix < minLength:
|
while commonPrefix < minLength:
|
||||||
eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"):
|
if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
|
||||||
|
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
|
||||||
inc commonPrefix
|
inc commonPrefix
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
# compute common suffix:
|
# compute common suffix:
|
||||||
var oldPos = oldLength - 1
|
var oldPos = oldLength - 1
|
||||||
var newPos = newLength - 1
|
var newPos = newLength - 1
|
||||||
while oldPos >= commonPrefix and newPos >= commonPrefix:
|
while oldPos >= commonPrefix and newPos >= commonPrefix:
|
||||||
eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"):
|
if eq(newNode[newPos], oldNode[oldPos], true) == identical:
|
||||||
|
kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
|
||||||
dec oldPos
|
dec oldPos
|
||||||
dec newPos
|
dec newPos
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
let pos = min(oldPos, newPos) + 1
|
let pos = min(oldPos, newPos) + 1
|
||||||
# now the different children are in commonPrefix .. pos - 1:
|
# now the different children are in commonPrefix .. pos - 1:
|
||||||
for i in commonPrefix..pos-1:
|
for i in commonPrefix..pos-1:
|
||||||
let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
|
diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
|
||||||
if r == usenewNode:
|
|
||||||
#oldNode[i] = newNode[i]
|
|
||||||
kxi.addPatchV(oldNode, i, newNode[i])
|
|
||||||
elif r != different:
|
|
||||||
newNode[i] = oldNode[i]
|
|
||||||
#else:
|
|
||||||
# result = usenewNode
|
|
||||||
|
|
||||||
if oldPos + 1 == oldLength:
|
if oldPos + 1 == oldLength:
|
||||||
for i in pos..newPos:
|
for i in pos..newPos:
|
||||||
kxi.addPatch(pkAppend, current, nil, newNode[i])
|
kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
|
||||||
result = usenewNode
|
|
||||||
else:
|
else:
|
||||||
let before = current.childNodes[oldPos + 1]
|
let before = current.childNodes[oldPos + 1]
|
||||||
for i in pos..newPos:
|
for i in pos..newPos:
|
||||||
kxi.addPatch(pkInsertBefore, current, before, newNode[i])
|
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
|
||||||
result = usenewNode
|
|
||||||
# XXX call 'attach' here?
|
# XXX call 'attach' here?
|
||||||
for i in pos..oldPos:
|
for i in pos..oldPos:
|
||||||
detach(oldNode[i])
|
detach(oldNode[i])
|
||||||
#doAssert i < current.childNodes.len
|
#doAssert i < current.childNodes.len
|
||||||
kxi.addPatch(pkRemove, current, current.childNodes[i], nil)
|
kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
|
||||||
result = usenewNode
|
|
||||||
of different:
|
of different:
|
||||||
detach(oldNode)
|
detach(oldNode)
|
||||||
kxi.addPatch(pkReplace, parent, current, newNode)
|
kxi.addPatch(pkReplace, parent, current, newNode, nil)
|
||||||
of usenewNode: doAssert(false, "eq returned usenewNode")
|
of usenewNode: doAssert(false, "eq returned usenewNode")
|
||||||
when defined(stats):
|
when defined(stats):
|
||||||
dec kxi.recursion
|
dec kxi.recursion
|
||||||
|
|
@ -470,11 +542,7 @@ proc applyComponents(kxi: KaraxInstance) =
|
||||||
let newNode = kxi.components[i].newNode
|
let newNode = kxi.components[i].newNode
|
||||||
when defined(karaxDebug):
|
when defined(karaxDebug):
|
||||||
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
|
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
|
||||||
if x.realDomImpl != nil:
|
if x.changedImpl != nil and x.changedImpl(x, newNode):
|
||||||
let current = kxi.components[i].current
|
|
||||||
let parent = kxi.components[i].parent
|
|
||||||
kxi.addPatch(pkReplace, parent, current, x)
|
|
||||||
elif x.changedImpl != nil and x.changedImpl(x, newNode):
|
|
||||||
when defined(karaxDebug):
|
when defined(karaxDebug):
|
||||||
echo "Component ", newNode.text, " did change"
|
echo "Component ", newNode.text, " did change"
|
||||||
let current = kxi.components[i].current
|
let current = kxi.components[i].current
|
||||||
|
|
@ -487,11 +555,12 @@ proc applyComponents(kxi: KaraxInstance) =
|
||||||
x.renderedVersion = x.version
|
x.renderedVersion = x.version
|
||||||
if oldExpanded == nil:
|
if oldExpanded == nil:
|
||||||
detach(x)
|
detach(x)
|
||||||
kxi.addPatch(pkReplace, parent, current, x.expanded)
|
kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
|
||||||
when defined(karaxDebug):
|
when defined(karaxDebug):
|
||||||
echo "Component ", newNode.text, ": old expansion didn't exist"
|
echo "Component ", newNode.text, ": old expansion didn't exist"
|
||||||
else:
|
else:
|
||||||
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
|
diff(x.expanded, oldExpanded, parent, current, kxi)
|
||||||
|
when false:
|
||||||
if res == usenewNode:
|
if res == usenewNode:
|
||||||
when defined(karaxDebug):
|
when defined(karaxDebug):
|
||||||
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
|
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
|
||||||
|
|
@ -519,7 +588,7 @@ when defined(stats):
|
||||||
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
|
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
|
||||||
detach(parent[position])
|
detach(parent[position])
|
||||||
let current = parent.dom
|
let current = parent.dom
|
||||||
kxi.addPatch(pkRemove, current, current.childNodes[position], nil)
|
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil)
|
||||||
parent.delete(position)
|
parent.delete(position)
|
||||||
applyPatch(kxi)
|
applyPatch(kxi)
|
||||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||||
|
|
@ -527,11 +596,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
|
||||||
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
|
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
|
||||||
let current = parent.dom
|
let current = parent.dom
|
||||||
if position >= parent.len:
|
if position >= parent.len:
|
||||||
kxi.addPatch(pkAppend, current, nil, kid)
|
kxi.addPatch(pkAppend, current, nil, kid, nil)
|
||||||
parent.add(kid)
|
parent.add(kid)
|
||||||
else:
|
else:
|
||||||
let before = current.childNodes[position]
|
let before = current.childNodes[position]
|
||||||
kxi.addPatch(pkInsertBefore, current, before, kid)
|
kxi.addPatch(pkInsertBefore, current, before, kid, nil)
|
||||||
parent.insert(kid, position)
|
parent.insert(kid, position)
|
||||||
applyPatch(kxi)
|
applyPatch(kxi)
|
||||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||||
|
|
@ -539,7 +608,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
|
||||||
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
|
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
|
||||||
let olddom = oldNode.dom
|
let olddom = oldNode.dom
|
||||||
doAssert olddom != nil
|
doAssert olddom != nil
|
||||||
discard diff(newNode, oldNode, nil, olddom, kxi)
|
diff(newNode, oldNode, nil, olddom, kxi)
|
||||||
# this is a bit nasty: Since we cannot patch the 'parent' of
|
# this is a bit nasty: Since we cannot patch the 'parent' of
|
||||||
# the current VNode (because we don't store it at all!), we
|
# the current VNode (because we don't store it at all!), we
|
||||||
# need to override the fields individually:
|
# need to override the fields individually:
|
||||||
|
|
@ -550,20 +619,44 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
|
||||||
kxi.currentTree = newNode
|
kxi.currentTree = newNode
|
||||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
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) =
|
proc dodraw(kxi: KaraxInstance) =
|
||||||
if kxi.renderer.isNil: return
|
if kxi.renderer.isNil: return
|
||||||
let newtree = kxi.renderer()
|
kxi.renderId = 0
|
||||||
|
|
||||||
|
if kxi.rendering:
|
||||||
|
# there is a render already running, delay 1 frame
|
||||||
|
kxi.renderId = reqFrame(proc () = kxi.dodraw)
|
||||||
|
return
|
||||||
|
|
||||||
|
kxi.rendering = true
|
||||||
|
|
||||||
|
let rdata = RouterData(hashPart: hashPart)
|
||||||
|
let newtree = kxi.renderer(rdata)
|
||||||
inc kxi.runCount
|
inc kxi.runCount
|
||||||
newtree.id = kxi.rootId
|
newtree.id = kxi.rootId
|
||||||
kxi.toFocus = nil
|
kxi.toFocus = nil
|
||||||
if kxi.currentTree == nil:
|
if kxi.currentTree == nil:
|
||||||
let asdom = vnodeToDom(newtree, kxi)
|
let asdom = toDom(newtree, useAttachedNode = true, kxi)
|
||||||
replaceById(kxi.rootId, asdom)
|
replaceById(kxi.rootId, asdom)
|
||||||
else:
|
else:
|
||||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||||
let olddom = document.getElementById(kxi.rootId)
|
let olddom = document.getElementById(kxi.rootId)
|
||||||
discard diff(newtree, kxi.currentTree, nil, olddom, kxi)
|
diff(newtree, kxi.currentTree, nil, olddom, kxi)
|
||||||
#kout cstring"patch len ", patches.len
|
|
||||||
when defined(profileKarax):
|
when defined(profileKarax):
|
||||||
echo "<<<<<<<<<<<<<<"
|
echo "<<<<<<<<<<<<<<"
|
||||||
echa reasons
|
echa reasons
|
||||||
|
|
@ -577,20 +670,17 @@ proc dodraw(kxi: KaraxInstance) =
|
||||||
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||||
|
|
||||||
if not kxi.postRenderCallback.isNil:
|
if not kxi.postRenderCallback.isNil:
|
||||||
kxi.postRenderCallback()
|
kxi.postRenderCallback(rdata)
|
||||||
|
|
||||||
# now that it's part of the DOM, give it the focus:
|
# now that it's part of the DOM, give it the focus:
|
||||||
if kxi.toFocus != nil:
|
if kxi.toFocus != nil:
|
||||||
kxi.toFocus.focus()
|
kxi.toFocus.focus()
|
||||||
kxi.renderId = 0
|
kxi.rendering = false
|
||||||
when defined(stats):
|
when defined(stats):
|
||||||
kxi.recursion = 0
|
kxi.recursion = 0
|
||||||
var total = 0
|
var total = 0
|
||||||
echo "depth ", depth(kxi.currentTree, total), " total ", total
|
echo "depth ", depth(kxi.currentTree, total), " total ", total
|
||||||
|
|
||||||
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
|
|
||||||
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
|
|
||||||
|
|
||||||
proc redraw*(kxi: KaraxInstance = kxi) =
|
proc redraw*(kxi: KaraxInstance = kxi) =
|
||||||
# we buffer redraw requests:
|
# we buffer redraw requests:
|
||||||
when false:
|
when false:
|
||||||
|
|
@ -608,26 +698,49 @@ proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi)
|
||||||
proc init(ev: Event) =
|
proc init(ev: Event) =
|
||||||
kxi.renderId = reqFrame(proc () = kxi.dodraw)
|
kxi.renderId = reqFrame(proc () = kxi.dodraw)
|
||||||
|
|
||||||
proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
|
proc setRenderer*(renderer: proc (data: RouterData): VNode,
|
||||||
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
|
root: cstring = "ROOT",
|
||||||
|
clientPostRenderCallback:
|
||||||
|
proc (data: RouterData) = nil): KaraxInstance {.
|
||||||
|
discardable.} =
|
||||||
## Setup Karax. Usually the return value can be ignored.
|
## 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,
|
result = KaraxInstance(rootId: root, renderer: renderer,
|
||||||
postRenderCallback: clientPostRenderCallback,
|
postRenderCallback: clientPostRenderCallback,
|
||||||
patches: newSeq[Patch](60),
|
patches: newSeq[Patch](60),
|
||||||
patchesV: newSeq[PatchV](30),
|
patchesV: newSeq[PatchV](30),
|
||||||
components: @[])
|
components: @[],
|
||||||
|
surpressRedraws: false,
|
||||||
|
byId: newJDict[cstring, VNode](),
|
||||||
|
orphans: newJDict[cstring, bool]())
|
||||||
kxi = result
|
kxi = result
|
||||||
window.onload = init
|
window.onload = init
|
||||||
|
onhashChange = proc() = redraw()
|
||||||
|
|
||||||
proc setInitializer*(renderer: proc (): VNode, root: cstring = "ROOT",
|
proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
|
||||||
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
|
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
|
||||||
## Setup Karax. Usually the return value can be ignored.
|
## 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,
|
result = KaraxInstance(rootId: root, renderer: renderer,
|
||||||
postRenderCallback: clientPostRenderCallback,
|
postRenderCallback: clientPostRenderCallback,
|
||||||
patches: newSeq[Patch](60),
|
patches: newSeq[Patch](60),
|
||||||
patchesV: newSeq[PatchV](30),
|
patchesV: newSeq[PatchV](30),
|
||||||
components: @[],
|
components: @[],
|
||||||
surpressRedraws: true)
|
surpressRedraws: true,
|
||||||
|
byId: newJDict[cstring, VNode](),
|
||||||
|
orphans: newJDict[cstring, bool]())
|
||||||
kxi = result
|
kxi = result
|
||||||
window.onload = init
|
window.onload = init
|
||||||
|
|
||||||
|
|
@ -655,16 +768,21 @@ proc addEventHandler*(n: VNode; k: EventKind; action: proc();
|
||||||
if not kxi.surpressRedraws: redraw(kxi)
|
if not kxi.surpressRedraws: redraw(kxi)
|
||||||
addEventListener(n, k, wrapper)
|
addEventListener(n, k, wrapper)
|
||||||
|
|
||||||
proc setOnHashChange*(action: proc (hashPart: cstring)) =
|
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
|
||||||
var onhashChange {.importc: "window.onhashchange".}: proc()
|
## Now deprecated, instead pass a callback to ``setRenderer`` that receives
|
||||||
var hashPart {.importc: "window.location.hash".}: cstring
|
## a ``data: RouterData`` parameter.
|
||||||
proc wrapper() =
|
proc wrapper() =
|
||||||
action(hashPart)
|
action(hashPart)
|
||||||
redraw()
|
redraw()
|
||||||
onhashchange = wrapper
|
onhashchange = wrapper
|
||||||
|
|
||||||
{.push stackTrace:off.}
|
proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) =
|
||||||
|
## Declares a node ID as "foreign". Foreign nodes are not
|
||||||
|
## under Karax's control in the sense that Karax does not attempt
|
||||||
|
## to perform structural checks on them.
|
||||||
|
kxi.orphans[id] = true
|
||||||
|
|
||||||
|
{.push stackTrace:off.}
|
||||||
proc setupErrorHandler*() =
|
proc setupErrorHandler*() =
|
||||||
## Installs an error handler that transforms native JS unhandled
|
## Installs an error handler that transforms native JS unhandled
|
||||||
## exceptions into Nim based stack traces. If `useAlert` is false,
|
## exceptions into Nim based stack traces. If `useAlert` is false,
|
||||||
|
|
@ -674,7 +792,7 @@ proc setupErrorHandler*() =
|
||||||
var onerror {.importc: "window.onerror", used.} =
|
var onerror {.importc: "window.onerror", used.} =
|
||||||
proc (msg, url: cstring, line, col: int, error: cstring): bool =
|
proc (msg, url: cstring, line, col: int, error: cstring): bool =
|
||||||
var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
|
var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
|
||||||
kout(x)
|
echo(x)
|
||||||
return true # suppressErrorAlert
|
return true # suppressErrorAlert
|
||||||
{.pop.}
|
{.pop.}
|
||||||
|
|
||||||
|
|
@ -682,9 +800,35 @@ proc prepend(parent, kid: Element) =
|
||||||
parent.insertBefore(kid, parent.firstChild)
|
parent.insertBefore(kid, parent.firstChild)
|
||||||
|
|
||||||
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
|
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
|
||||||
let body = document.getElementById("body")
|
|
||||||
let s = document.createElement("script")
|
let s = document.createElement("script")
|
||||||
s.setAttr "type", "text/javascript"
|
s.setAttr "type", "text/javascript"
|
||||||
s.setAttr "src", jsfilename
|
s.setAttr "src", jsfilename
|
||||||
body.prepend(s)
|
document.body.prepend(s)
|
||||||
redraw(kxi)
|
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)
|
||||||
|
|
@ -1,25 +1,46 @@
|
||||||
|
|
||||||
import macros, karax, vdom, compact
|
import macros, vdom, compact, kbase
|
||||||
from strutils import startsWith, toLowerAscii
|
from strutils import startsWith, toLowerAscii
|
||||||
|
|
||||||
|
when defined(js):
|
||||||
|
import karax
|
||||||
|
|
||||||
const
|
const
|
||||||
StmtContext = ["kout", "inc", "echo", "dec", "!"]
|
StmtContext = ["kout", "inc", "echo", "dec", "!"]
|
||||||
SpecialAttrs = ["id", "class", "value", "index", "style"]
|
SpecialAttrs = ["id", "class", "value", "index", "style"]
|
||||||
|
|
||||||
proc getName(n: NimNode): string =
|
proc getName(n: NimNode): string =
|
||||||
case n.kind
|
case n.kind
|
||||||
of nnkIdent:
|
of nnkIdent, nnkSym:
|
||||||
result = $n.ident
|
result = $n
|
||||||
of nnkAccQuoted:
|
of nnkAccQuoted:
|
||||||
result = ""
|
result = ""
|
||||||
for i in 0..<n.len:
|
for i in 0..<n.len:
|
||||||
result.add getName(n[i])
|
result.add getName(n[i])
|
||||||
of nnkStrLit..nnkTripleStrLit:
|
of nnkStrLit..nnkTripleStrLit:
|
||||||
result = n.strVal
|
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:
|
else:
|
||||||
#echo repr n
|
#echo repr n
|
||||||
expectKind(n, nnkIdent)
|
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 =
|
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
|
||||||
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
|
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
|
||||||
|
|
||||||
|
|
@ -32,6 +53,11 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||||
# (except for the last child of the macros we consider here),
|
# (except for the last child of the macros we consider here),
|
||||||
# lets, consts, types can be considered as expressions
|
# lets, consts, types can be considered as expressions
|
||||||
# case is complex, calls are assumed to produce a value.
|
# 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
|
case n.kind
|
||||||
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
|
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
|
||||||
if tmpContext != nil:
|
if tmpContext != nil:
|
||||||
|
|
@ -68,7 +94,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||||
if tmpContext == nil:
|
if tmpContext == nil:
|
||||||
error "no VNode to attach the event handler to"
|
error "no VNode to attach the event handler to"
|
||||||
else:
|
else:
|
||||||
result = newCall(bindSym"addEventHandler", tmpContext,
|
result = newCall(evHandler(), tmpContext,
|
||||||
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
|
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
|
||||||
else:
|
else:
|
||||||
result = n
|
result = n
|
||||||
|
|
@ -94,10 +120,14 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||||
if x.kind == nnkExprEqExpr:
|
if x.kind == nnkExprEqExpr:
|
||||||
let key = getName x[0]
|
let key = getName x[0]
|
||||||
if key.startsWith("on"):
|
if key.startsWith("on"):
|
||||||
result.add newCall(bindSym"addEventHandler",
|
result.add newCall(evHandler(),
|
||||||
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
|
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:
|
elif key in SpecialAttrs:
|
||||||
result.add newDotAsgn(tmp, key, x[1])
|
result.add newDotAsgn(tmp, key, x[1])
|
||||||
|
if key == "value":
|
||||||
|
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
|
||||||
elif eqIdent(key, "setFocus"):
|
elif eqIdent(key, "setFocus"):
|
||||||
result.add newCall(key, tmp, x[1], ident"kxi")
|
result.add newCall(key, tmp, x[1], ident"kxi")
|
||||||
elif eqIdent(key, "events"):
|
elif eqIdent(key, "events"):
|
||||||
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
|
||||||
|
|
@ -37,6 +37,38 @@ type
|
||||||
onsubmit*: proc (event: Event) {.nimcall.}
|
onsubmit*: proc (event: Event) {.nimcall.}
|
||||||
onunload*: proc (event: Event) {.nimcall.}
|
onunload*: proc (event: Event) {.nimcall.}
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/Events
|
||||||
|
DomEvent* {.pure.} = enum
|
||||||
|
Abort = "abort",
|
||||||
|
BeforeInput = "beforeinput",
|
||||||
|
Blur = "blur",
|
||||||
|
Click = "click",
|
||||||
|
CompositionEnd = "compositionend",
|
||||||
|
CompositionStart = "compositionstart",
|
||||||
|
CompositionUpdate = "compositionupdate",
|
||||||
|
DblClick = "dblclick",
|
||||||
|
Error = "error",
|
||||||
|
Focus = "focus",
|
||||||
|
FocusIn = "focusin",
|
||||||
|
FocusOut = "focusout",
|
||||||
|
Input = "input",
|
||||||
|
KeyDown = "keydown",
|
||||||
|
KeyPress = "keypress",
|
||||||
|
KeyUp = "keyup",
|
||||||
|
Load = "load",
|
||||||
|
MouseDown = "mousedown",
|
||||||
|
MouseEnter = "mouseenter",
|
||||||
|
MouseLeave = "mouseleave",
|
||||||
|
MouseMove = "mousemove",
|
||||||
|
MouseOut = "mouseout",
|
||||||
|
MouseOver = "mouseover",
|
||||||
|
MouseUp = "mouseup",
|
||||||
|
Resize = "resize",
|
||||||
|
Scroll = "scroll",
|
||||||
|
Select = "select",
|
||||||
|
Unload = "unload",
|
||||||
|
Wheel = "wheel"
|
||||||
|
|
||||||
Window* = ref WindowObj
|
Window* = ref WindowObj
|
||||||
WindowObj {.importc.} = object of EventTargetObj
|
WindowObj {.importc.} = object of EventTargetObj
|
||||||
document*: Document
|
document*: Document
|
||||||
|
|
@ -57,6 +89,7 @@ type
|
||||||
status*: cstring
|
status*: cstring
|
||||||
toolbar*: ref ToolBar
|
toolbar*: ref ToolBar
|
||||||
frames*: seq[Frame]
|
frames*: seq[Frame]
|
||||||
|
screen*: Screen
|
||||||
|
|
||||||
Frame* = ref FrameObj
|
Frame* = ref FrameObj
|
||||||
FrameObj {.importc.} = object of WindowObj
|
FrameObj {.importc.} = object of WindowObj
|
||||||
|
|
@ -131,9 +164,8 @@ type
|
||||||
name*: cstring
|
name*: cstring
|
||||||
readOnly*: bool
|
readOnly*: bool
|
||||||
options*: seq[OptionElement]
|
options*: seq[OptionElement]
|
||||||
|
selectedOptions*: seq[OptionElement]
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
|
clientWidth*, clientHeight*: int
|
||||||
HtmlElement* = ref object of Element
|
|
||||||
contentEditable*: string
|
contentEditable*: string
|
||||||
isContentEditable*: bool
|
isContentEditable*: bool
|
||||||
dir*: string
|
dir*: string
|
||||||
|
|
@ -142,6 +174,87 @@ type
|
||||||
offsetLeft*: int
|
offsetLeft*: int
|
||||||
offsetTop*: int
|
offsetTop*: int
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
|
||||||
|
ValidityState* = ref ValidityStateObj
|
||||||
|
ValidityStateObj {.importc.} = object
|
||||||
|
badInput*: bool
|
||||||
|
customError*: bool
|
||||||
|
patternMismatch*: bool
|
||||||
|
rangeOverflow*: bool
|
||||||
|
rangeUnderflow*: bool
|
||||||
|
stepMismatch*: bool
|
||||||
|
tooLong*: bool
|
||||||
|
tooShort*: bool
|
||||||
|
typeMismatch*: bool
|
||||||
|
valid*: bool
|
||||||
|
valueMissing*: bool
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
||||||
|
Blob* = ref BlobObj
|
||||||
|
BlobObj {.importc.} = object of RootObj
|
||||||
|
size*: int
|
||||||
|
`type`*: cstring
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/File
|
||||||
|
File* = ref FileObj
|
||||||
|
FileObj {.importc.} = object of Blob
|
||||||
|
lastModified*: int
|
||||||
|
name*: cstring
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement
|
||||||
|
InputElement* = ref InputElementObj
|
||||||
|
InputElementObj {.importc.} = object of Element
|
||||||
|
# Properties related to the parent form
|
||||||
|
formAction*: cstring
|
||||||
|
formEncType*: cstring
|
||||||
|
formMethod*: cstring
|
||||||
|
formNoValidate*: bool
|
||||||
|
formTarget*: cstring
|
||||||
|
|
||||||
|
# Properties that apply to any type of input element that is not hidden
|
||||||
|
`type`*: cstring
|
||||||
|
autofocus*: bool
|
||||||
|
required*: bool
|
||||||
|
value*: cstring
|
||||||
|
validity*: ValidityState
|
||||||
|
validationMessage*: cstring
|
||||||
|
willValidate*: bool
|
||||||
|
|
||||||
|
# Properties that apply only to elements of type "checkbox" or "radio"
|
||||||
|
indeterminate*: bool
|
||||||
|
|
||||||
|
# Properties that apply only to elements of type "image"
|
||||||
|
alt*: cstring
|
||||||
|
height*: cstring
|
||||||
|
src*: cstring
|
||||||
|
width*: cstring
|
||||||
|
|
||||||
|
# Properties that apply only to elements of type "file"
|
||||||
|
accept*: cstring
|
||||||
|
files*: seq[Blob]
|
||||||
|
|
||||||
|
# Properties that apply only to text/number-containing or elements
|
||||||
|
autocomplete*: cstring
|
||||||
|
maxLength*: int
|
||||||
|
size*: int
|
||||||
|
pattern*: cstring
|
||||||
|
placeholder*: cstring
|
||||||
|
min*: cstring
|
||||||
|
max*: cstring
|
||||||
|
selectionStart*: int
|
||||||
|
selectionEnd*: int
|
||||||
|
selectionDirection*: cstring
|
||||||
|
|
||||||
|
# Properties not yet categorized
|
||||||
|
dirName*: cstring
|
||||||
|
accessKey*: cstring
|
||||||
|
list*: Element
|
||||||
|
multiple*: bool
|
||||||
|
labels*: seq[Element]
|
||||||
|
step*: cstring
|
||||||
|
valueAsDate*: cstring
|
||||||
|
valueAsNumber*: float
|
||||||
|
|
||||||
LinkElement* = ref LinkObj
|
LinkElement* = ref LinkObj
|
||||||
LinkObj {.importc.} = object of ElementObj
|
LinkObj {.importc.} = object of ElementObj
|
||||||
target*: cstring
|
target*: cstring
|
||||||
|
|
@ -171,13 +284,19 @@ type
|
||||||
text*: cstring
|
text*: cstring
|
||||||
value*: cstring
|
value*: cstring
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
|
||||||
FormElement* = ref FormObj
|
FormElement* = ref FormObj
|
||||||
FormObj {.importc.} = object of ElementObj
|
FormObj {.importc.} = object of ElementObj
|
||||||
|
acceptCharset*: cstring
|
||||||
action*: cstring
|
action*: cstring
|
||||||
encoding*: cstring
|
autocomplete*: cstring
|
||||||
`method`*: cstring
|
|
||||||
target*: cstring
|
|
||||||
elements*: seq[Element]
|
elements*: seq[Element]
|
||||||
|
encoding*: cstring
|
||||||
|
enctype*: cstring
|
||||||
|
length*: int
|
||||||
|
`method`*: cstring
|
||||||
|
noValidate*: bool
|
||||||
|
target*: cstring
|
||||||
|
|
||||||
ImageElement* = ref ImageObj
|
ImageElement* = ref ImageObj
|
||||||
ImageObj {.importc.} = object of ElementObj
|
ImageObj {.importc.} = object of ElementObj
|
||||||
|
|
@ -284,55 +403,467 @@ type
|
||||||
wordSpacing*: cstring
|
wordSpacing*: cstring
|
||||||
zIndex*: int
|
zIndex*: int
|
||||||
|
|
||||||
# TODO: A lot of the fields in Event belong to a more specific type of event.
|
EventPhase* = enum
|
||||||
# TODO: Should we clean this up?
|
None = 0,
|
||||||
|
CapturingPhase,
|
||||||
|
AtTarget,
|
||||||
|
BubblingPhase
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/Event
|
||||||
Event* = ref EventObj
|
Event* = ref EventObj
|
||||||
EventObj {.importc.} = object of RootObj
|
EventObj {.importc.} = object of RootObj
|
||||||
|
bubbles*: bool
|
||||||
|
cancelBubble*: bool
|
||||||
|
cancelable*: bool
|
||||||
|
composed*: bool
|
||||||
|
currentTarget*: Node
|
||||||
|
defaultPrevented*: bool
|
||||||
|
eventPhase*: int
|
||||||
target*: Node
|
target*: Node
|
||||||
altKey*, ctrlKey*, shiftKey*: bool
|
`type`*: cstring
|
||||||
button*: int
|
isTrusted*: bool
|
||||||
clientX*, clientY*: int
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
||||||
|
UIEvent* = ref UIEventObj
|
||||||
|
UIEventObj {.importc.} = object of Event
|
||||||
|
detail*: int64
|
||||||
|
view*: Window
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
|
||||||
|
KeyboardEvent* = ref KeyboardEventObj
|
||||||
|
KeyboardEventObj {.importc.} = object of UIEvent
|
||||||
|
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
|
||||||
|
code*: cstring
|
||||||
|
isComposing*: bool
|
||||||
|
key*: cstring
|
||||||
keyCode*: int
|
keyCode*: int
|
||||||
layerX*, layerY*: int
|
location*: int
|
||||||
modifiers*: int
|
|
||||||
ALT_MASK*, CONTROL_MASK*, SHIFT_MASK*, META_MASK*: int
|
# https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
||||||
|
KeyboardEventKey* {.pure.} = enum
|
||||||
|
# Modifier keys
|
||||||
|
Alt,
|
||||||
|
AltGraph,
|
||||||
|
CapsLock,
|
||||||
|
Control,
|
||||||
|
Fn,
|
||||||
|
FnLock,
|
||||||
|
Hyper,
|
||||||
|
Meta,
|
||||||
|
NumLock,
|
||||||
|
ScrollLock,
|
||||||
|
Shift,
|
||||||
|
Super,
|
||||||
|
Symbol,
|
||||||
|
SymbolLock,
|
||||||
|
|
||||||
|
# Whitespace keys
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowUp,
|
||||||
|
End,
|
||||||
|
Home,
|
||||||
|
PageDown,
|
||||||
|
PageUp,
|
||||||
|
|
||||||
|
# Editing keys
|
||||||
|
Backspace,
|
||||||
|
Clear,
|
||||||
|
Copy,
|
||||||
|
CrSel,
|
||||||
|
Cut,
|
||||||
|
Delete,
|
||||||
|
EraseEof,
|
||||||
|
ExSel,
|
||||||
|
Insert,
|
||||||
|
Paste,
|
||||||
|
Redo,
|
||||||
|
Undo,
|
||||||
|
|
||||||
|
# UI keys
|
||||||
|
Accept,
|
||||||
|
Again,
|
||||||
|
Attn,
|
||||||
|
Cancel,
|
||||||
|
ContextMenu,
|
||||||
|
Escape,
|
||||||
|
Execute,
|
||||||
|
Find,
|
||||||
|
Finish,
|
||||||
|
Help,
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
Props,
|
||||||
|
Select,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
|
||||||
|
# Device keys
|
||||||
|
BrigtnessDown,
|
||||||
|
BrigtnessUp,
|
||||||
|
Eject,
|
||||||
|
LogOff,
|
||||||
|
Power,
|
||||||
|
PowerOff,
|
||||||
|
PrintScreen,
|
||||||
|
Hibernate,
|
||||||
|
Standby,
|
||||||
|
WakeUp,
|
||||||
|
|
||||||
|
# Common IME keys
|
||||||
|
AllCandidates,
|
||||||
|
Alphanumeric,
|
||||||
|
CodeInput,
|
||||||
|
Compose,
|
||||||
|
Convert,
|
||||||
|
Dead,
|
||||||
|
FinalMode,
|
||||||
|
GroupFirst,
|
||||||
|
GroupLast,
|
||||||
|
GroupNext,
|
||||||
|
GroupPrevious,
|
||||||
|
ModeChange,
|
||||||
|
NextCandidate,
|
||||||
|
NonConvert,
|
||||||
|
PreviousCandidate,
|
||||||
|
Process,
|
||||||
|
SingleCandidate,
|
||||||
|
|
||||||
|
# Korean keyboards only
|
||||||
|
HangulMode,
|
||||||
|
HanjaMode,
|
||||||
|
JunjaMode,
|
||||||
|
|
||||||
|
# Japanese keyboards only
|
||||||
|
Eisu,
|
||||||
|
Hankaku,
|
||||||
|
Hiragana,
|
||||||
|
HiraganaKatakana,
|
||||||
|
KanaMode,
|
||||||
|
KanjiMode,
|
||||||
|
Katakana,
|
||||||
|
Romaji,
|
||||||
|
Zenkaku,
|
||||||
|
ZenkakuHanaku,
|
||||||
|
|
||||||
|
# Function keys
|
||||||
|
F1,
|
||||||
|
F2,
|
||||||
|
F3,
|
||||||
|
F4,
|
||||||
|
F5,
|
||||||
|
F6,
|
||||||
|
F7,
|
||||||
|
F8,
|
||||||
|
F9,
|
||||||
|
F10,
|
||||||
|
F11,
|
||||||
|
F12,
|
||||||
|
F13,
|
||||||
|
F14,
|
||||||
|
F15,
|
||||||
|
F16,
|
||||||
|
F17,
|
||||||
|
F18,
|
||||||
|
F19,
|
||||||
|
F20,
|
||||||
|
Soft1,
|
||||||
|
Soft2,
|
||||||
|
Soft3,
|
||||||
|
Soft4,
|
||||||
|
|
||||||
|
# Phone keys
|
||||||
|
AppSwitch,
|
||||||
|
Call,
|
||||||
|
Camera,
|
||||||
|
CameraFocus,
|
||||||
|
EndCall,
|
||||||
|
GoBack,
|
||||||
|
GoHome,
|
||||||
|
HeadsetHook,
|
||||||
|
LastNumberRedial,
|
||||||
|
Notification,
|
||||||
|
MannerMode,
|
||||||
|
VoiceDial,
|
||||||
|
|
||||||
|
# Multimedia keys
|
||||||
|
ChannelDown,
|
||||||
|
ChannelUp,
|
||||||
|
MediaFastForward,
|
||||||
|
MediaPause,
|
||||||
|
MediaPlay,
|
||||||
|
MediaPlayPause,
|
||||||
|
MediaRecord,
|
||||||
|
MediaRewind,
|
||||||
|
MediaStop,
|
||||||
|
MediaTrackNext,
|
||||||
|
MediaTrackPrevious,
|
||||||
|
|
||||||
|
# Audio control keys
|
||||||
|
AudioBalanceLeft,
|
||||||
|
AudioBalanceRight,
|
||||||
|
AudioBassDown,
|
||||||
|
AudioBassBoostDown,
|
||||||
|
AudioBassBoostToggle,
|
||||||
|
AudioBassBoostUp,
|
||||||
|
AudioBassUp,
|
||||||
|
AudioFaderFront,
|
||||||
|
AudioFaderRear,
|
||||||
|
AudioSurroundModeNext,
|
||||||
|
AudioTrebleDown,
|
||||||
|
AudioTrebleUp,
|
||||||
|
AudioVolumeDown,
|
||||||
|
AUdioVolumeMute,
|
||||||
|
AudioVolumeUp,
|
||||||
|
MicrophoneToggle,
|
||||||
|
MicrophoneVolumeDown,
|
||||||
|
MicrophoneVolumeMute,
|
||||||
|
MicrophoneVolumeUp,
|
||||||
|
|
||||||
|
# TV control keys
|
||||||
|
TV,
|
||||||
|
TV3DMode,
|
||||||
|
TVAntennaCable,
|
||||||
|
TVAudioDescription,
|
||||||
|
TVAudioDescriptionMixDown,
|
||||||
|
TVAudioDescriptionMixUp,
|
||||||
|
TVContentsMenu,
|
||||||
|
TVDataService,
|
||||||
|
TVInput,
|
||||||
|
TVInputComponent1,
|
||||||
|
TVInputComponent2,
|
||||||
|
TVInputComposite1,
|
||||||
|
TVInputComposite2,
|
||||||
|
TVInputHDMI1,
|
||||||
|
TVInputHDMI2,
|
||||||
|
TVInputHDMI3,
|
||||||
|
TVInputHDMI4,
|
||||||
|
TVInputVGA1,
|
||||||
|
TVMediaContext,
|
||||||
|
TVNetwork,
|
||||||
|
TVNumberEntry,
|
||||||
|
TVPower,
|
||||||
|
TVRadioService,
|
||||||
|
TVSatellite,
|
||||||
|
TVSatelliteBS,
|
||||||
|
TVSatelliteCS,
|
||||||
|
TVSatelliteToggle,
|
||||||
|
TVTerrestrialAnalog,
|
||||||
|
TVTerrestrialDigital,
|
||||||
|
TVTimer,
|
||||||
|
|
||||||
|
# Media controller keys
|
||||||
|
AVRInput,
|
||||||
|
AVRPower,
|
||||||
|
ColorF0Red,
|
||||||
|
ColorF1Green,
|
||||||
|
ColorF2Yellow,
|
||||||
|
ColorF3Blue,
|
||||||
|
ColorF4Grey,
|
||||||
|
ColorF5Brown,
|
||||||
|
ClosedCaptionToggle,
|
||||||
|
Dimmer,
|
||||||
|
DisplaySwap,
|
||||||
|
DVR,
|
||||||
|
Exit,
|
||||||
|
FavoriteClear0,
|
||||||
|
FavoriteClear1,
|
||||||
|
FavoriteClear2,
|
||||||
|
FavoriteClear3,
|
||||||
|
FavoriteRecall0,
|
||||||
|
FavoriteRecall1,
|
||||||
|
FavoriteRecall2,
|
||||||
|
FavoriteRecall3,
|
||||||
|
FavoriteStore0,
|
||||||
|
FavoriteStore1,
|
||||||
|
FavoriteStore2,
|
||||||
|
FavoriteStore3,
|
||||||
|
Guide,
|
||||||
|
GuideNextDay,
|
||||||
|
GuidePreviousDay,
|
||||||
|
Info,
|
||||||
|
InstantReplay,
|
||||||
|
Link,
|
||||||
|
ListProgram,
|
||||||
|
LiveContent,
|
||||||
|
Lock,
|
||||||
|
MediaApps,
|
||||||
|
MediaAudioTrack,
|
||||||
|
MediaLast,
|
||||||
|
MediaSkipBackward,
|
||||||
|
MediaSkipForward,
|
||||||
|
MediaStepBackward,
|
||||||
|
MediaStepForward,
|
||||||
|
MediaTopMenu,
|
||||||
|
NavigateIn,
|
||||||
|
NavigateNext,
|
||||||
|
NavigateOut,
|
||||||
|
NavigatePrevious,
|
||||||
|
NextFavoriteChannel,
|
||||||
|
NextUserProfile,
|
||||||
|
OnDemand,
|
||||||
|
Pairing,
|
||||||
|
PinPDown,
|
||||||
|
PinPMove,
|
||||||
|
PinPUp,
|
||||||
|
PlaySpeedDown,
|
||||||
|
PlaySpeedReset,
|
||||||
|
PlaySpeedUp,
|
||||||
|
RandomToggle,
|
||||||
|
RcLowBattery,
|
||||||
|
RecordSpeedNext,
|
||||||
|
RfBypass,
|
||||||
|
ScanChannelsToggle,
|
||||||
|
ScreenModeNext,
|
||||||
|
Settings,
|
||||||
|
SplitScreenToggle,
|
||||||
|
STBInput,
|
||||||
|
STBPower,
|
||||||
|
Subtitle,
|
||||||
|
Teletext,
|
||||||
|
VideoModeNext,
|
||||||
|
Wink,
|
||||||
|
ZoomToggle,
|
||||||
|
|
||||||
|
# Speech recognition keys
|
||||||
|
SpeechCorrectionList,
|
||||||
|
SpeechInputToggle,
|
||||||
|
|
||||||
|
# Document keys
|
||||||
|
Close,
|
||||||
|
New,
|
||||||
|
Open,
|
||||||
|
Print,
|
||||||
|
Save,
|
||||||
|
SpellCheck,
|
||||||
|
MailForward,
|
||||||
|
MailReply,
|
||||||
|
MailSend,
|
||||||
|
|
||||||
|
# Application selector keys
|
||||||
|
LaunchCalculator,
|
||||||
|
LaunchCalendar,
|
||||||
|
LaunchContacts,
|
||||||
|
LaunchMail,
|
||||||
|
LaunchMediaPlayer,
|
||||||
|
LaunchMusicPlayer,
|
||||||
|
LaunchMyComputer,
|
||||||
|
LaunchPhone,
|
||||||
|
LaunchScreenSaver,
|
||||||
|
LaunchSpreadsheet,
|
||||||
|
LaunchWebBrowser,
|
||||||
|
LaunchWebCam,
|
||||||
|
LaunchWordProcessor,
|
||||||
|
LaunchApplication1,
|
||||||
|
LaunchApplication2,
|
||||||
|
LaunchApplication3,
|
||||||
|
LaunchApplication4,
|
||||||
|
LaunchApplication5,
|
||||||
|
LaunchApplication6,
|
||||||
|
LaunchApplication7,
|
||||||
|
LaunchApplication8,
|
||||||
|
LaunchApplication9,
|
||||||
|
LaunchApplication10,
|
||||||
|
LaunchApplication11,
|
||||||
|
LaunchApplication12,
|
||||||
|
LaunchApplication13,
|
||||||
|
LaunchApplication14,
|
||||||
|
LaunchApplication15,
|
||||||
|
LaunchApplication16,
|
||||||
|
|
||||||
|
# Browser control keys
|
||||||
|
BrowserBack,
|
||||||
|
BrowserFavorites,
|
||||||
|
BrowserForward,
|
||||||
|
BrowserHome,
|
||||||
|
BrowserRefresh,
|
||||||
|
BrowserSearch,
|
||||||
|
BrowserStop,
|
||||||
|
|
||||||
|
# Numeric keypad keys
|
||||||
|
Key11,
|
||||||
|
Key12,
|
||||||
|
Separator
|
||||||
|
|
||||||
|
MouseButtons* = enum
|
||||||
|
NoButton = 0,
|
||||||
|
PrimaryButton = 1,
|
||||||
|
SecondaryButton = 2,
|
||||||
|
AuxilaryButton = 4,
|
||||||
|
FourthButton = 8,
|
||||||
|
FifthButton = 16
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
|
||||||
|
MouseEvent* = ref MouseEventObj
|
||||||
|
MouseEventObj {.importc.} = object of UIEvent
|
||||||
|
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
|
||||||
|
button*: int
|
||||||
|
buttons*: int
|
||||||
|
clientX*, clientY*: int
|
||||||
|
movementX*, movementY*: int
|
||||||
offsetX*, offsetY*: int
|
offsetX*, offsetY*: int
|
||||||
pageX*, pageY*: int
|
pageX*, pageY*: int
|
||||||
|
relatedTarget*: EventTarget
|
||||||
|
#region*: cstring
|
||||||
screenX*, screenY*: int
|
screenX*, screenY*: int
|
||||||
which*: int
|
|
||||||
`type`*: cstring
|
|
||||||
x*, y*: int
|
x*, y*: int
|
||||||
ABORT*: int
|
|
||||||
BLUR*: int
|
DataTransferItemKind* {.pure.} = enum
|
||||||
CHANGE*: int
|
File = "file",
|
||||||
CLICK*: int
|
String = "string"
|
||||||
DBLCLICK*: int
|
|
||||||
DRAGDROP*: int
|
# https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem
|
||||||
ERROR*: int
|
DataTransferItem* = ref DataTransferItemObj
|
||||||
FOCUS*: int
|
DataTransferItemObj {.importc.} = object of RootObj
|
||||||
KEYDOWN*: int
|
kind*: cstring
|
||||||
KEYPRESS*: int
|
`type`*: cstring
|
||||||
KEYUP*: int
|
|
||||||
LOAD*: int
|
# https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer
|
||||||
MOUSEDOWN*: int
|
DataTransfer* = ref DataTransferObj
|
||||||
MOUSEMOVE*: int
|
DataTransferObj {.importc.} = object of RootObj
|
||||||
MOUSEOUT*: int
|
dropEffect*: cstring
|
||||||
MOUSEOVER*: int
|
effectAllowed*: cstring
|
||||||
MOUSEUP*: int
|
files*: seq[Element]
|
||||||
MOVE*: int
|
items*: seq[DataTransferItem]
|
||||||
RESET*: int
|
types*: seq[cstring]
|
||||||
RESIZE*: int
|
|
||||||
SELECT*: int
|
DataTransferDropEffect* {.pure.} = enum
|
||||||
SUBMIT*: int
|
None = "none",
|
||||||
UNLOAD*: int
|
Copy = "copy",
|
||||||
|
Link = "link",
|
||||||
|
Move = "move"
|
||||||
|
|
||||||
|
DataTransferEffectAllowed* {.pure.} = enum
|
||||||
|
None = "none",
|
||||||
|
Copy = "copy",
|
||||||
|
CopyLink = "copyLink",
|
||||||
|
CopyMove = "copyMove",
|
||||||
|
Link = "link",
|
||||||
|
LinkMove = "linkMove",
|
||||||
|
Move = "move",
|
||||||
|
All = "all",
|
||||||
|
Uninitialized = "uninitialized"
|
||||||
|
|
||||||
|
DragEventTypes* = enum
|
||||||
|
Drag = "drag",
|
||||||
|
DragEnd = "dragend",
|
||||||
|
DragEnter = "dragenter",
|
||||||
|
DragExit = "dragexit",
|
||||||
|
DragLeave = "dragleave",
|
||||||
|
DragOver = "dragover",
|
||||||
|
DragStart = "dragstart",
|
||||||
|
Drop = "drop"
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/DragEvent
|
||||||
|
DragEvent* {.importc.} = object of MouseEvent
|
||||||
|
dataTransfer*: DataTransfer
|
||||||
|
|
||||||
TouchList* {.importc.} = ref object of RootObj
|
TouchList* {.importc.} = ref object of RootObj
|
||||||
length*: int
|
length*: int
|
||||||
|
|
||||||
TouchEvent* {.importc.} = ref object of Event
|
Touch* = ref TouchObj
|
||||||
changedTouches*, targetTouches*, touches*: TouchList
|
TouchObj {.importc.} = object of RootObj
|
||||||
|
|
||||||
Touch* {.importc.} = ref object of RootObj
|
|
||||||
identifier*: int
|
identifier*: int
|
||||||
screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int
|
screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int
|
||||||
target*: Element
|
target*: Element
|
||||||
|
|
@ -340,6 +871,10 @@ type
|
||||||
rotationAngle*: int
|
rotationAngle*: int
|
||||||
force*: float
|
force*: float
|
||||||
|
|
||||||
|
TouchEvent* = ref TouchEventObj
|
||||||
|
TouchEventObj {.importc.} = object of UIEvent
|
||||||
|
changedTouches*, targetTouches*, touches*: seq[Touch]
|
||||||
|
|
||||||
Location* = ref LocationObj
|
Location* = ref LocationObj
|
||||||
LocationObj {.importc.} = object of RootObj
|
LocationObj {.importc.} = object of RootObj
|
||||||
hash*: cstring
|
hash*: cstring
|
||||||
|
|
@ -397,6 +932,11 @@ type
|
||||||
TimeOut* {.importc.} = ref object of RootObj
|
TimeOut* {.importc.} = ref object of RootObj
|
||||||
Interval* {.importc.} = object of RootObj
|
Interval* {.importc.} = object of RootObj
|
||||||
|
|
||||||
|
AddEventListenerOptions* = object
|
||||||
|
capture*: bool
|
||||||
|
once*: bool
|
||||||
|
passive*: bool
|
||||||
|
|
||||||
when defined(nodejs):
|
when defined(nodejs):
|
||||||
# we provide a dummy DOM for nodejs for testing purposes
|
# we provide a dummy DOM for nodejs for testing purposes
|
||||||
proc len*(x: Node): int = x.childNodes.len
|
proc len*(x: Node): int = x.childNodes.len
|
||||||
|
|
@ -483,6 +1023,7 @@ proc clearTimeout*(t: Timeout) {.importc, nodecl.}
|
||||||
|
|
||||||
# EventTarget "methods"
|
# EventTarget "methods"
|
||||||
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), useCapture: bool = false)
|
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), useCapture: bool = false)
|
||||||
|
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), options: AddEventListenerOptions)
|
||||||
proc removeEventListener*(et: EventTarget; ev: cstring; cb: proc(ev: Event))
|
proc removeEventListener*(et: EventTarget; ev: cstring; cb: proc(ev: Event))
|
||||||
|
|
||||||
# Window "methods"
|
# Window "methods"
|
||||||
|
|
@ -491,7 +1032,7 @@ proc back*(w: Window)
|
||||||
proc blur*(w: Window)
|
proc blur*(w: Window)
|
||||||
proc captureEvents*(w: Window, eventMask: int) {.deprecated.}
|
proc captureEvents*(w: Window, eventMask: int) {.deprecated.}
|
||||||
proc clearInterval*(w: Window, interval: ref Interval)
|
proc clearInterval*(w: Window, interval: ref Interval)
|
||||||
proc clearTimeout*(w: Window, timeout: TimeOut)
|
proc clearTimeout*(w: Window, timeout: ref TimeOut)
|
||||||
proc close*(w: Window)
|
proc close*(w: Window)
|
||||||
proc confirm*(w: Window, msg: cstring): bool
|
proc confirm*(w: Window, msg: cstring): bool
|
||||||
proc disableExternalCapture*(w: Window)
|
proc disableExternalCapture*(w: Window)
|
||||||
|
|
@ -500,6 +1041,7 @@ proc find*(w: Window, text: cstring, caseSensitive = false,
|
||||||
backwards = false)
|
backwards = false)
|
||||||
proc focus*(w: Window)
|
proc focus*(w: Window)
|
||||||
proc forward*(w: Window)
|
proc forward*(w: Window)
|
||||||
|
proc getComputedStyle*(w: Window, e: Node, pe:Node = nil): Style
|
||||||
proc handleEvent*(w: Window, e: Event)
|
proc handleEvent*(w: Window, e: Event)
|
||||||
proc home*(w: Window)
|
proc home*(w: Window)
|
||||||
proc moveBy*(w: Window, x, y: int)
|
proc moveBy*(w: Window, x, y: int)
|
||||||
|
|
@ -516,7 +1058,7 @@ proc scrollBy*(w: Window, x, y: int)
|
||||||
proc scrollTo*(w: Window, x, y: int)
|
proc scrollTo*(w: Window, x, y: int)
|
||||||
proc setInterval*(w: Window, code: cstring, pause: int): ref Interval
|
proc setInterval*(w: Window, code: cstring, pause: int): ref Interval
|
||||||
proc setInterval*(w: Window, function: proc (), pause: int): ref Interval
|
proc setInterval*(w: Window, function: proc (), pause: int): ref Interval
|
||||||
proc setTimeout*(w: Window, code: cstring, pause: int): TimeOut
|
proc setTimeout*(w: Window, code: cstring, pause: int): ref TimeOut
|
||||||
proc setTimeout*(w: Window, function: proc (), pause: int): ref Interval
|
proc setTimeout*(w: Window, function: proc (), pause: int): ref Interval
|
||||||
proc stop*(w: Window)
|
proc stop*(w: Window)
|
||||||
proc requestAnimationFrame*(w: Window, function: proc (time: float)): int
|
proc requestAnimationFrame*(w: Window, function: proc (time: float)): int
|
||||||
|
|
@ -526,6 +1068,7 @@ proc cancelAnimationFrame*(w: Window, id: int)
|
||||||
proc appendData*(n: Node, data: cstring)
|
proc appendData*(n: Node, data: cstring)
|
||||||
proc cloneNode*(n: Node, copyContent: bool): Node
|
proc cloneNode*(n: Node, copyContent: bool): Node
|
||||||
proc deleteData*(n: Node, start, len: int)
|
proc deleteData*(n: Node, start, len: int)
|
||||||
|
proc focus*(e: Node)
|
||||||
proc getAttribute*(n: Node, attr: cstring): cstring
|
proc getAttribute*(n: Node, attr: cstring): cstring
|
||||||
proc getAttributeNode*(n: Node, attr: cstring): Node
|
proc getAttributeNode*(n: Node, attr: cstring): Node
|
||||||
proc hasChildNodes*(n: Node): bool
|
proc hasChildNodes*(n: Node): bool
|
||||||
|
|
@ -533,7 +1076,7 @@ proc insertData*(n: Node, position: int, data: cstring)
|
||||||
proc removeAttribute*(n: Node, attr: cstring)
|
proc removeAttribute*(n: Node, attr: cstring)
|
||||||
proc removeAttributeNode*(n, attr: Node)
|
proc removeAttributeNode*(n, attr: Node)
|
||||||
proc replaceData*(n: Node, start, len: int, text: cstring)
|
proc replaceData*(n: Node, start, len: int, text: cstring)
|
||||||
proc scrollIntoView*(n: Node)
|
proc scrollIntoView*(n: Node, alignToTop = true)
|
||||||
proc setAttribute*(n: Node, name, value: cstring)
|
proc setAttribute*(n: Node, name, value: cstring)
|
||||||
proc setAttributeNode*(n: Node, attr: Node)
|
proc setAttributeNode*(n: Node, attr: Node)
|
||||||
|
|
||||||
|
|
@ -550,11 +1093,13 @@ proc releaseEvents*(d: Document, eventMask: int) {.deprecated.}
|
||||||
proc routeEvent*(d: Document, event: Event)
|
proc routeEvent*(d: Document, event: Event)
|
||||||
proc write*(d: Document, text: cstring)
|
proc write*(d: Document, text: cstring)
|
||||||
proc writeln*(d: Document, text: cstring)
|
proc writeln*(d: Document, text: cstring)
|
||||||
|
proc querySelector*(d: Document, selectors: cstring): Element
|
||||||
|
proc querySelectorAll*(d: Document, selectors: cstring): seq[Element]
|
||||||
|
|
||||||
# Element "methods"
|
# Element "methods"
|
||||||
proc blur*(e: Element)
|
proc blur*(e: Element)
|
||||||
proc click*(e: Element)
|
proc click*(e: Element)
|
||||||
proc focus*(e: Node)
|
proc focus*(e: Element)
|
||||||
proc handleEvent*(e: Element, event: Event)
|
proc handleEvent*(e: Element, event: Event)
|
||||||
proc select*(e: Element)
|
proc select*(e: Element)
|
||||||
proc getElementsByTagName*(e: Element, name: cstring): seq[Element]
|
proc getElementsByTagName*(e: Element, name: cstring): seq[Element]
|
||||||
|
|
@ -563,6 +1108,8 @@ proc getElementsByClassName*(e: Element, name: cstring): seq[Element]
|
||||||
# FormElement "methods"
|
# FormElement "methods"
|
||||||
proc reset*(f: FormElement)
|
proc reset*(f: FormElement)
|
||||||
proc submit*(f: FormElement)
|
proc submit*(f: FormElement)
|
||||||
|
proc checkValidity*(e: FormElement): bool
|
||||||
|
proc reportValidity*(e: FormElement): bool
|
||||||
|
|
||||||
# EmbedElement "methods"
|
# EmbedElement "methods"
|
||||||
proc play*(e: EmbedElement)
|
proc play*(e: EmbedElement)
|
||||||
|
|
@ -593,12 +1140,37 @@ proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false)
|
||||||
|
|
||||||
# Event "methods"
|
# Event "methods"
|
||||||
proc preventDefault*(ev: Event)
|
proc preventDefault*(ev: Event)
|
||||||
|
proc stopImmediatePropagation*(ev: Event)
|
||||||
proc stopPropagation*(ev: Event)
|
proc stopPropagation*(ev: Event)
|
||||||
|
|
||||||
|
# KeyboardEvent "methods"
|
||||||
|
proc getModifierState*(ev: KeyboardEvent, keyArg: cstring): bool
|
||||||
|
|
||||||
|
# MouseEvent "methods"
|
||||||
|
proc getModifierState*(ev: MouseEvent, keyArg: cstring): bool
|
||||||
|
|
||||||
# TouchEvent "methods"
|
# TouchEvent "methods"
|
||||||
proc identifiedTouch*(list: TouchList): Touch
|
proc identifiedTouch*(list: TouchList): Touch
|
||||||
proc item*(list: TouchList, i: int): Touch
|
proc item*(list: TouchList, i: int): Touch
|
||||||
|
|
||||||
|
# DataTransfer "methods"
|
||||||
|
proc clearData*(dt: DataTransfer, format: cstring)
|
||||||
|
proc getData*(dt: DataTransfer, format: cstring): cstring
|
||||||
|
proc setData*(dt: DataTransfer, format: cstring, data: cstring)
|
||||||
|
proc setDragImage*(dt: DataTransfer, img: Element, xOffset: int64, yOffset: int64)
|
||||||
|
|
||||||
|
# DataTransferItem "methods"
|
||||||
|
proc getAsFile*(dti: DataTransferItem): File
|
||||||
|
|
||||||
|
# InputElement "methods"
|
||||||
|
proc setSelectionRange*(e: InputElement, selectionStart: int, selectionEnd: int, selectionDirection: cstring = "none")
|
||||||
|
proc setRangeText*(e: InputElement, replacement: cstring, startindex: int = 0, endindex: int = 0, selectionMode: cstring = "preserve")
|
||||||
|
proc setCustomValidity*(e: InputElement, error: cstring)
|
||||||
|
proc checkValidity*(e: InputElement): bool
|
||||||
|
|
||||||
|
# Blob "methods"
|
||||||
|
proc slice*(e: Blob, startindex: int = 0, endindex: int = e.size, contentType: cstring = "")
|
||||||
|
|
||||||
{.pop.}
|
{.pop.}
|
||||||
|
|
||||||
proc setAttr*(n: Node; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
|
proc setAttr*(n: Node; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
|
||||||
|
|
@ -657,3 +1229,4 @@ proc inViewport*(el: Node): bool =
|
||||||
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
|
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
|
||||||
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
|
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
|
||||||
proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
|
proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
|
||||||
|
proc offsetLeft*(e: Node): int {.importcpp: "#.offsetLeft", nodecl.}
|
||||||
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
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
## Include file that contains the common imports for the Karax framework.
|
## Include file that contains the common imports for the Karax framework.
|
||||||
|
|
||||||
import karax, karaxdsl, vdom, compact
|
import kbase, karax, karaxdsl, vdom, compact, jstrutils
|
||||||
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()
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
## Virtual DOM implementation for Karax.
|
## Virtual DOM implementation for Karax.
|
||||||
|
|
||||||
|
when defined(js):
|
||||||
from kdom import Event, Node
|
from kdom import Event, Node
|
||||||
import shash, macros, vstyles
|
else:
|
||||||
from strutils import toUpperAscii
|
type
|
||||||
|
Event* = ref object
|
||||||
|
Node* = ref object
|
||||||
|
|
||||||
|
import macros, vstyles, kbase
|
||||||
|
from strutils import toUpperAscii, toLowerAscii, tokenize
|
||||||
|
|
||||||
type
|
type
|
||||||
VNodeKind* {.pure.} = enum
|
VNodeKind* {.pure.} = enum
|
||||||
text = "#text", int = "#int", bool = "#bool",
|
text = "#text", int = "#int", bool = "#bool",
|
||||||
vthunk = "#vthunk", dthunk = "#dthunk",
|
vthunk = "#vthunk", dthunk = "#dthunk",
|
||||||
component = "#component",
|
component = "#component", verbatim = "#verbatim",
|
||||||
|
|
||||||
html, head, title, base, link, meta, style,
|
html, head, title, base, link, meta, style,
|
||||||
script, noscript,
|
script, noscript,
|
||||||
body, section, nav, article, aside,
|
body, section, nav, article, aside,
|
||||||
h1, h2, h3, h4, h5, h6,
|
h1, h2, h3, h4, h5, h6,
|
||||||
header, footer, address, main
|
header, footer, address, main,
|
||||||
|
|
||||||
p, hr, pre, blockquote, ol, ul, li,
|
p, hr, pre, blockquote, ol, ul, li,
|
||||||
dl, dt, dd,
|
dl, dt, dd,
|
||||||
|
|
@ -29,7 +35,19 @@ type
|
||||||
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
|
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
|
||||||
ins, del, img, iframe, embed, `object` = "object",
|
ins, del, img, iframe, embed, `object` = "object",
|
||||||
param, video, audio, source, track, canvas, map,
|
param, video, audio, source, track, canvas, map,
|
||||||
area, svg, math,
|
area, math,
|
||||||
|
|
||||||
|
# SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html
|
||||||
|
animate, animateMotion, animateTransform, circle, clipPath, defs, desc,
|
||||||
|
`discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer,
|
||||||
|
feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap,
|
||||||
|
feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR,
|
||||||
|
feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset,
|
||||||
|
fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence,
|
||||||
|
filter, foreignObject, g, image, line, linearGradient, marker, mask,
|
||||||
|
metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect,
|
||||||
|
`set` = "set", stop, svg, switch, symbol, txt = "text", textPath, tspan,
|
||||||
|
unknown, use, view,
|
||||||
|
|
||||||
table, caption, colgroup, col, tbody, thead,
|
table, caption, colgroup, col, tbody, thead,
|
||||||
tfoot, tr, td, th,
|
tfoot, tr, td, th,
|
||||||
|
|
@ -39,6 +57,10 @@ type
|
||||||
keygen, output, progress, meter,
|
keygen, output, progress, meter,
|
||||||
details, summary, command, menu
|
details, summary, command, menu
|
||||||
|
|
||||||
|
const selfClosing = {area, base, br, col, embed, hr, img, input,
|
||||||
|
link, meta, param, source, track, wbr}
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
|
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
|
||||||
onclick, ## An element is clicked.
|
onclick, ## An element is clicked.
|
||||||
|
|
@ -78,25 +100,36 @@ type
|
||||||
onsubmit, ## A form is submitted
|
onsubmit, ## A form is submitted
|
||||||
oninput, ## An input value changes
|
oninput, ## An input value changes
|
||||||
|
|
||||||
onkeyupenter, ## vdom extension: an input field received the ENTER key press
|
onanimationstart,
|
||||||
onkeyuplater ## vdom extension: a key was pressed and some time
|
onanimationend,
|
||||||
## passed (useful for on-the-fly text completions)
|
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 =
|
macro buildLookupTables(): untyped =
|
||||||
var a = newTree(nnkBracket)
|
var a = newTree(nnkBracket)
|
||||||
for i in low(VNodeKind)..high(VNodeKind):
|
for i in low(VNodeKind)..high(VNodeKind):
|
||||||
let x = $i
|
let x = $i
|
||||||
let y = if x[0] == '#': x else: toUpperAscii(x)
|
let y = if x[0] == '#': x else: toUpperAscii(x)
|
||||||
a.add(newCall("cstring", newLit(y)))
|
a.add(newCall("kstring", newLit(y)))
|
||||||
var e = newTree(nnkBracket)
|
var e = newTree(nnkBracket)
|
||||||
for i in low(EventKind)..high(EventKind):
|
for i in low(EventKind)..high(EventKind):
|
||||||
e.add(newCall("cstring", newLit(substr($i, 2))))
|
e.add(newCall("kstring", newLit(substr($i, 2))))
|
||||||
|
|
||||||
template tmpl(a, e) {.dirty.} =
|
template tmpl(a, e) {.dirty.} =
|
||||||
const
|
const
|
||||||
toTag*: array[VNodeKind, cstring] = a
|
toTag*: array[VNodeKind, kstring] = a
|
||||||
toEventName*: array[EventKind, cstring] = e
|
toEventName*: array[EventKind, kstring] = e
|
||||||
|
|
||||||
result = getAst tmpl(a, e)
|
result = getAst tmpl(a, e)
|
||||||
|
|
||||||
|
|
@ -108,15 +141,15 @@ type
|
||||||
|
|
||||||
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
|
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
|
||||||
|
|
||||||
VKey* = cstring
|
VKey* = kstring
|
||||||
|
|
||||||
VNode* = ref object of RootObj
|
VNode* = ref object of RootObj
|
||||||
kind*: VNodeKind
|
kind*: VNodeKind
|
||||||
index*: int ## a generally useful 'index'
|
index*: int ## a generally useful 'index'
|
||||||
id*, class*, text*: cstring
|
id*, class*, text*: kstring
|
||||||
kids: seq[VNode]
|
kids: seq[VNode]
|
||||||
# even index: key, odd index: value; done this way for memory efficiency:
|
# even index: key, odd index: value; done this way for memory efficiency:
|
||||||
attrs: seq[cstring]
|
attrs: seq[kstring]
|
||||||
events*: EventHandlers
|
events*: EventHandlers
|
||||||
when false:
|
when false:
|
||||||
hash*: Hash
|
hash*: Hash
|
||||||
|
|
@ -133,7 +166,6 @@ type
|
||||||
updatedImpl*: proc(self, newInstance: VComponent)
|
updatedImpl*: proc(self, newInstance: VComponent)
|
||||||
onAttachImpl*: proc(self: VComponent)
|
onAttachImpl*: proc(self: VComponent)
|
||||||
onDetachImpl*: proc(self: VComponent)
|
onDetachImpl*: proc(self: VComponent)
|
||||||
realDomImpl*: proc(self: VComponent): kdom.Node
|
|
||||||
version*: int ## Update this to trigger a redraw by karax. Usually you
|
version*: int ## Update this to trigger a redraw by karax. Usually you
|
||||||
## should call 'markDirty' instead which is an alias for
|
## should call 'markDirty' instead which is an alias for
|
||||||
## 'inc version'.
|
## 'inc version'.
|
||||||
|
|
@ -143,23 +175,23 @@ type
|
||||||
## expanded to.
|
## expanded to.
|
||||||
debugId*: int
|
debugId*: int
|
||||||
|
|
||||||
proc value*(n: VNode): cstring = n.text
|
proc value*(n: VNode): kstring = n.text
|
||||||
proc `value=`*(n: VNode; v: cstring) = n.text = v
|
proc `value=`*(n: VNode; v: kstring) = n.text = v
|
||||||
|
|
||||||
proc intValue*(n: VNode): int = n.index
|
proc intValue*(n: VNode): int = n.index
|
||||||
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i)
|
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*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b))
|
||||||
proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
|
proc vn*(x: kstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
|
||||||
|
|
||||||
template callThunk*(fn: typed; n: VNode): untyped =
|
template callThunk*(fn: typed; n: VNode): untyped =
|
||||||
## for internal usage only.
|
## for internal usage only.
|
||||||
fn(n.kids)
|
fn(n.kids)
|
||||||
|
|
||||||
proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
|
proc vthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
|
||||||
VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
|
VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
|
||||||
|
|
||||||
proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
|
proc dthunk*(dom: Node): VNode =
|
||||||
VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args)
|
VNode(kind: VNodeKind.dthunk, dom: dom)
|
||||||
|
|
||||||
proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
|
proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
|
||||||
assert handler != nil
|
assert handler != nil
|
||||||
|
|
@ -193,7 +225,7 @@ template newComponent*[T](t: typeDesc[T];
|
||||||
updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T =
|
updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T =
|
||||||
## Use this template to create new components.
|
## Use this template to create new components.
|
||||||
T(kind: VNodeKind.component, index: -1,
|
T(kind: VNodeKind.component, index: -1,
|
||||||
text: cstring(astToStr(t)), renderImpl: render,
|
text: kstring(astToStr(t)), renderImpl: render,
|
||||||
changedImpl: changed, updatedImpl: updated,
|
changedImpl: changed, updatedImpl: updated,
|
||||||
onAttachImpl: onAttach, onDetachImpl: onDetach,
|
onAttachImpl: onAttach, onDetachImpl: onDetach,
|
||||||
debugId: getDebugId())
|
debugId: getDebugId())
|
||||||
|
|
@ -202,8 +234,8 @@ template markDirty*(c: VComponent) =
|
||||||
## mark the component as dirty so that it is re-rendered.
|
## mark the component as dirty so that it is re-rendered.
|
||||||
inc c.version
|
inc c.version
|
||||||
|
|
||||||
proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
|
proc setAttr*(n: VNode; key: kstring; val: kstring = "") =
|
||||||
if n.attrs.isNil:
|
if n.attrs.len == 0:
|
||||||
n.attrs = @[key, val]
|
n.attrs = @[key, val]
|
||||||
else:
|
else:
|
||||||
for i in countup(0, n.attrs.len-2, 2):
|
for i in countup(0, n.attrs.len-2, 2):
|
||||||
|
|
@ -213,7 +245,7 @@ proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
|
||||||
n.attrs.add key
|
n.attrs.add key
|
||||||
n.attrs.add val
|
n.attrs.add val
|
||||||
|
|
||||||
proc getAttr*(n: VNode; key: cstring): cstring =
|
proc getAttr*(n: VNode; key: kstring): kstring =
|
||||||
for i in countup(0, n.attrs.len-2, 2):
|
for i in countup(0, n.attrs.len-2, 2):
|
||||||
if n.attrs[i] == key: return n.attrs[i+1]
|
if n.attrs[i] == key: return n.attrs[i+1]
|
||||||
|
|
||||||
|
|
@ -237,7 +269,12 @@ proc takeOverFields*(newNode, oldNode: VNode) =
|
||||||
proc len*(x: VNode): int = x.kids.len
|
proc len*(x: VNode): int = x.kids.len
|
||||||
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
|
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
|
||||||
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
|
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
|
||||||
proc add*(parent, kid: VNode) = parent.kids.add kid
|
|
||||||
|
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) =
|
proc delete*(parent: VNode; position: int) =
|
||||||
parent.kids.delete(position)
|
parent.kids.delete(position)
|
||||||
proc insert*(parent, kid: VNode; position: int) =
|
proc insert*(parent, kid: VNode; position: int) =
|
||||||
|
|
@ -248,18 +285,26 @@ proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
|
||||||
result = newVNode(kind)
|
result = newVNode(kind)
|
||||||
for k in kids: result.add k
|
for k in kids: result.add k
|
||||||
|
|
||||||
proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)];
|
proc tree*(kind: VNodeKind; attrs: openarray[(kstring, kstring)];
|
||||||
kids: varargs[VNode]): VNode =
|
kids: varargs[VNode]): VNode =
|
||||||
result = tree(kind, kids)
|
result = tree(kind, kids)
|
||||||
for a in attrs: result.setAttr(a[0], a[1])
|
for a in attrs: result.setAttr(a[0], a[1])
|
||||||
|
|
||||||
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1)
|
when defined(js):
|
||||||
proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1)
|
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 =
|
iterator items*(n: VNode): VNode =
|
||||||
for i in 0..<n.kids.len: yield n.kids[i]
|
for i in 0..<n.kids.len: yield n.kids[i]
|
||||||
|
|
||||||
iterator attrs*(n: VNode): (cstring, cstring) =
|
iterator attrs*(n: VNode): (kstring, kstring) =
|
||||||
for i in countup(0, n.attrs.len-2, 2):
|
for i in countup(0, n.attrs.len-2, 2):
|
||||||
yield (n.attrs[i], n.attrs[i+1])
|
yield (n.attrs[i], n.attrs[i+1])
|
||||||
|
|
||||||
|
|
@ -272,8 +317,14 @@ proc sameAttrs*(a, b: VNode): bool =
|
||||||
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
|
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
|
||||||
n.events.add((event, handler, nil))
|
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) =
|
template toStringAttr(field) =
|
||||||
if n.field != nil:
|
if n.field.len > 0:
|
||||||
result.add " " & astToStr(field) & " = " & $n.field
|
result.add " " & astToStr(field) & " = " & $n.field
|
||||||
|
|
||||||
proc toString*(n: VNode; result: var string; indent: int) =
|
proc toString*(n: VNode; result: var string; indent: int) =
|
||||||
|
|
@ -288,7 +339,7 @@ proc toString*(n: VNode; result: var string; indent: int) =
|
||||||
if n.kind == VNodeKind.text:
|
if n.kind == VNodeKind.text:
|
||||||
result.add n.text
|
result.add n.text
|
||||||
else:
|
else:
|
||||||
if n.text != nil:
|
if n.text.len > 0:
|
||||||
result.add " value = "
|
result.add " value = "
|
||||||
result.add n.text
|
result.add n.text
|
||||||
for child in items(n):
|
for child in items(n):
|
||||||
|
|
@ -324,7 +375,116 @@ when false:
|
||||||
h &= child.hash
|
h &= child.hash
|
||||||
n.hash = h
|
n.hash = h
|
||||||
|
|
||||||
proc `$`*(n: VNode): cstring =
|
|
||||||
|
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 = ""
|
var res = ""
|
||||||
toString(n, res, 0)
|
toString(n, res, 0)
|
||||||
result = cstring(res)
|
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
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
|
##[
|
||||||
|
see examples/hellostyle.nim
|
||||||
|
]##
|
||||||
|
|
||||||
import kdom, macros, jdict
|
import std/[macros, strutils]
|
||||||
|
import kbase
|
||||||
|
|
||||||
|
when defined(js):
|
||||||
|
import kdom, jdict
|
||||||
|
|
||||||
type
|
type
|
||||||
StyleAttr* {.pure.} = enum
|
StyleAttr* {.pure.} = enum
|
||||||
|
|
@ -200,10 +207,10 @@ type
|
||||||
macro buildLookupTables(): untyped =
|
macro buildLookupTables(): untyped =
|
||||||
var e = newTree(nnkBracket)
|
var e = newTree(nnkBracket)
|
||||||
for i in low(StyleAttr)..high(StyleAttr):
|
for i in low(StyleAttr)..high(StyleAttr):
|
||||||
e.add(newCall("cstring", newLit($i)))
|
e.add(newCall("kstring", newLit($i)))
|
||||||
template tmpl(e) {.dirty.} =
|
template tmpl(e) {.dirty.} =
|
||||||
const
|
const
|
||||||
toStyleAttrName: array[StyleAttr, cstring] = e
|
toStyleAttrName: array[StyleAttr, kstring] = e
|
||||||
result = getAst tmpl(e)
|
result = getAst tmpl(e)
|
||||||
|
|
||||||
buildLookupTables()
|
buildLookupTables()
|
||||||
|
|
@ -212,8 +219,14 @@ buildLookupTables()
|
||||||
# it's relevant.
|
# it's relevant.
|
||||||
# even index: key, odd index: value; done this way for memory efficiency:
|
# even index: key, odd index: value; done this way for memory efficiency:
|
||||||
|
|
||||||
|
when defined(js):
|
||||||
type
|
type
|
||||||
VStyle* = JSeq[cstring]
|
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 =
|
proc eq*(a, b: VStyle): bool =
|
||||||
if a.isNil:
|
if a.isNil:
|
||||||
|
|
@ -225,19 +238,20 @@ proc eq*(a, b: VStyle): bool =
|
||||||
if a[i] != b[i]: return false
|
if a[i] != b[i]: return false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
proc kout[T](x: T) {.importc: "console.log", varargs.}
|
proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
|
||||||
|
## inserts (a, value) in sorted order of key `a`
|
||||||
proc setAttr(s: VStyle; a, value: cstring) {.noSideEffect.} =
|
# worst case quadratic complexity (if given styles in reverse order), hopefully
|
||||||
|
# not a concern assuming small cardinal
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < s.len:
|
while i < s.len:
|
||||||
if s[i] == a:
|
if s[i] == a:
|
||||||
s[i+1] = value
|
s[i+1] = value
|
||||||
return
|
return
|
||||||
elif s[i] > a:
|
elif s[i] > a:
|
||||||
s.add nil
|
s.add ""
|
||||||
s.add nil
|
s.add ""
|
||||||
# insertion point here:
|
# insertion point here, shift all remaining pairs by 2 indexes
|
||||||
for j in countdown(s.len-1, i, 2):
|
for j in countdown(s.len-1, i+3, 2):
|
||||||
s[j] = s[j-2]
|
s[j] = s[j-2]
|
||||||
s[j-1] = s[j-3]
|
s[j-1] = s[j-3]
|
||||||
s[i] = a
|
s[i] = a
|
||||||
|
|
@ -247,41 +261,61 @@ proc setAttr(s: VStyle; a, value: cstring) {.noSideEffect.} =
|
||||||
s.add a
|
s.add a
|
||||||
s.add value
|
s.add value
|
||||||
|
|
||||||
proc setAttr*(s: VStyle; attr: StyleAttr, value: cstring) {.noSideEffect.} =
|
proc setAttr*(s: VStyle; attr: StyleAttr, value: kstring) {.noSideEffect.} =
|
||||||
|
when kstring is cstring:
|
||||||
assert value != nil, "value must not be nil"
|
assert value != nil, "value must not be nil"
|
||||||
setAttr(s, toStyleAttrName[attr], value)
|
setAttr(s, toStyleAttrName[attr], value)
|
||||||
|
|
||||||
proc getAttr*(s: VStyle; attr: StyleAttr): cstring {.noSideEffect.} =
|
proc getAttr*(s: VStyle; attr: StyleAttr): kstring {.noSideEffect.} =
|
||||||
## returns 'nil' if the attribute has not been set.
|
## returns "" if the attribute has not been set.
|
||||||
var i = 0
|
var i = 0
|
||||||
let a = toStyleAttrName[attr]
|
let a = toStyleAttrName[attr]
|
||||||
while i < s.len:
|
while i < s.len:
|
||||||
if s[i] == a:
|
if s[i] == a:
|
||||||
return s[i+1]
|
return s[i+1]
|
||||||
elif s[i] > a:
|
elif s[i] > a:
|
||||||
return nil
|
return ""
|
||||||
inc i, 2
|
inc i, 2
|
||||||
|
|
||||||
proc style*(pairs: varargs[(StyleAttr, cstring)]): VStyle {.noSideEffect.} =
|
proc style*(pairs: varargs[(StyleAttr, kstring)]): VStyle {.noSideEffect.} =
|
||||||
## constructs a VStyle object from a list of (attribute, value)-pairs.
|
## constructs a VStyle object from a list of (attribute, value)-pairs.
|
||||||
|
when defined(js):
|
||||||
result = newJSeq[cstring]()
|
result = newJSeq[cstring]()
|
||||||
|
else:
|
||||||
|
new(result)
|
||||||
|
result[] = @[]
|
||||||
for x in pairs:
|
for x in pairs:
|
||||||
result.setAttr x[0], x[1]
|
result.setAttr x[0], x[1]
|
||||||
|
|
||||||
proc style*(a: StyleAttr; val: cstring): VStyle {.noSideEffect.} =
|
proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
|
||||||
## constructs a VStyle object from a single (attribute, value)-pair.
|
## constructs a VStyle object from a single (attribute, value)-pair.
|
||||||
|
when defined(js):
|
||||||
result = newJSeq[cstring]()
|
result = newJSeq[cstring]()
|
||||||
|
else:
|
||||||
|
new(result)
|
||||||
|
result[] = @[]
|
||||||
result.setAttr a, val
|
result.setAttr a, val
|
||||||
|
|
||||||
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
|
proc toCss*(a: string): VStyle =
|
||||||
|
##[
|
||||||
proc merge*(a, b: VStyle): VStyle {.noSideEffect.} =
|
See example in hellostyle.nim
|
||||||
## merges two styles. ``b`` takes precedence over ``a``.
|
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]()
|
result = newJSeq[cstring]()
|
||||||
for i in 0..<a.len:
|
else:
|
||||||
result.add a[i]
|
new(result)
|
||||||
for i in countup(0, b.len-1, 2):
|
result[] = @[]
|
||||||
setAttr(result, b[i], b[i+1])
|
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.} =
|
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
|
||||||
## apply the style to the real DOM node ``n``.
|
## apply the style to the real DOM node ``n``.
|
||||||
|
|
@ -291,15 +325,30 @@ proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
|
||||||
for i in countup(0, s.len-1, 2):
|
for i in countup(0, s.len-1, 2):
|
||||||
n.style.setStyle(s[i], s[i+1])
|
n.style.setStyle(s[i], s[i+1])
|
||||||
|
|
||||||
iterator pairs*(s: VStyle): (cstring, cstring) {.noSideEffect.} =
|
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:
|
if s != nil:
|
||||||
for i in countup(0, s.len-1, 2):
|
for i in countup(0, s.len-1, 2):
|
||||||
yield (s[i], s[i+1])
|
yield (s[i], s[i+1])
|
||||||
|
|
||||||
|
when defined(js):
|
||||||
import jstrutils
|
import jstrutils
|
||||||
|
else:
|
||||||
|
template `&`(x: untyped): untyped = $x
|
||||||
|
|
||||||
proc rgb*(r, g, b: range[0..255]): cstring =
|
proc rgb*(r, g, b: range[0..255]): kstring =
|
||||||
cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring")"
|
kstring"rgb(" & &r & kstring", " & &g & kstring", " & &b & kstring")"
|
||||||
|
|
||||||
proc rgba*(r, g, b: range[0..255], alpha: float): cstring =
|
proc rgba*(r, g, b: range[0..255], alpha: float): kstring =
|
||||||
cstring"rgba(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")"
|
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).
|
||||||
38
readme.rst
38
readme.rst
|
|
@ -1,38 +0,0 @@
|
||||||
Karax – Single page applications in Nim |travis|
|
|
||||||
================================================
|
|
||||||
|
|
||||||
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::
|
|
||||||
|
|
||||||
nimble install # For first time
|
|
||||||
|
|
||||||
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. But don't take my
|
|
||||||
word for it, look at this:
|
|
||||||
|
|
||||||
.. image:: docs/benchmark.png
|
|
||||||
|
|
||||||
|
|
||||||
Goals
|
|
||||||
=====
|
|
||||||
|
|
||||||
- Leverage Nim's macro system to produce a framework that allows
|
|
||||||
for the development of applications that are boilerplate free.
|
|
||||||
- Keep it small, keep it fast, keep it flexible.
|
|
||||||
|
|
||||||
.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master
|
|
||||||
:target: https://travis-ci.org/pragmagic/karax
|
|
||||||
121
src/compact.nim
121
src/compact.nim
|
|
@ -1,121 +0,0 @@
|
||||||
## Components in Karax are built by the ``.component`` macro annotation.
|
|
||||||
|
|
||||||
import macros, jdict, kdom, vdom, tables, strutils
|
|
||||||
|
|
||||||
var
|
|
||||||
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
|
|
||||||
dcomponents* = newJDict[cstring, proc(args: seq[VNode]): Node]()
|
|
||||||
|
|
||||||
type
|
|
||||||
ComponentKind* {.pure.} = enum
|
|
||||||
None,
|
|
||||||
Tag,
|
|
||||||
VNode,
|
|
||||||
Node
|
|
||||||
|
|
||||||
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.symbol)
|
|
||||||
else:
|
|
||||||
result = n
|
|
||||||
|
|
||||||
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
|
|
||||||
elif rettype == "Node":
|
|
||||||
isvirtual = ComponentKind.Node
|
|
||||||
else:
|
|
||||||
error "component must return VNode or Node", 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 dwrapper(pname, unpackCall) {.dirty.} =
|
|
||||||
proc pname(args: seq[VNode]): Node =
|
|
||||||
unpackCall
|
|
||||||
|
|
||||||
template vregister(key, val) =
|
|
||||||
bind jdict.`[]=`
|
|
||||||
`[]=`(vcomponents, cstring(key), val)
|
|
||||||
|
|
||||||
template dregister(key, val) =
|
|
||||||
bind jdict.`[]=`
|
|
||||||
`[]=`(dcomponents, cstring(key), val)
|
|
||||||
|
|
||||||
result = newTree(nnkStmtList, n)
|
|
||||||
|
|
||||||
if isvirtual == ComponentKind.VNode:
|
|
||||||
result.add getAst(vwrapper(newname name, unpackCall))
|
|
||||||
result.add getAst(vregister(newLit(nn), realName))
|
|
||||||
else:
|
|
||||||
result.add getAst(dwrapper(newname name, unpackCall))
|
|
||||||
result.add getAst(dregister(newLit(nn), realName))
|
|
||||||
allcomponents[nn] = isvirtual
|
|
||||||
when defined(debugKaraxDsl):
|
|
||||||
echo repr result
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
## Karax -- Single page applications for Nim.
|
|
||||||
|
|
||||||
## This module implements support for `ajax`:idx: socket
|
|
||||||
## handling.
|
|
||||||
|
|
||||||
import karax
|
|
||||||
|
|
||||||
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
|
|
||||||
data: cstring;
|
|
||||||
cont: proc (httpStatus: int; response: cstring);
|
|
||||||
kxi: KaraxInstance = kxi) =
|
|
||||||
proc contWrapper(httpStatus: int; response: cstring) =
|
|
||||||
cont(httpStatus, response)
|
|
||||||
redraw(kxi)
|
|
||||||
|
|
||||||
type
|
|
||||||
HttpRequest {.importc.} = ref object
|
|
||||||
ThisObj {.importc.} = ref object
|
|
||||||
readyState, status: int
|
|
||||||
responseText, statusText: cstring
|
|
||||||
|
|
||||||
proc setRequestHeader(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
|
|
||||||
proc statechange(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
|
|
||||||
proc send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
|
|
||||||
proc open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
|
|
||||||
proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
|
|
||||||
|
|
||||||
var this {.importc: "this".}: ThisObj
|
|
||||||
let ajax = newRequest()
|
|
||||||
ajax.open(meth, url, true)
|
|
||||||
for a, b in items(headers):
|
|
||||||
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.statusText)
|
|
||||||
ajax.send(data)
|
|
||||||
|
|
||||||
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
|
|
||||||
data: cstring;
|
|
||||||
cont: proc (httpStatus: int, response: cstring);
|
|
||||||
kxi: KaraxInstance = kxi) =
|
|
||||||
ajax("POST", url, headers, data, cont, kxi)
|
|
||||||
|
|
||||||
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
|
|
||||||
cont: proc (httpStatus: int, response: cstring);
|
|
||||||
kxi: KaraxInstance = kxi) =
|
|
||||||
ajax("GET", url, headers, nil, cont, kxi)
|
|
||||||
|
|
||||||
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
|
|
||||||
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
include karaxprelude
|
include ../karax/prelude
|
||||||
|
|
||||||
block:
|
block:
|
||||||
type
|
type
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/diffDomTests.js"></script>
|
<script type="text/javascript" src="diffDomTests.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
onload(window.location.pathname.split('/')[1])
|
onload(window.location.pathname.split('/')[1])
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
include "../src/karax"
|
include "../karax/karax"
|
||||||
import "../src/karaxdsl"
|
import "../karax/karaxdsl"
|
||||||
|
|
||||||
proc hasDom(n: Vnode) =
|
proc hasDom(n: Vnode) =
|
||||||
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
|
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
|
||||||
|
|
@ -22,15 +22,19 @@ proc shortRepr(n: VNode): string =
|
||||||
var err = 0
|
var err = 0
|
||||||
|
|
||||||
proc doDiff(a, b: VNode; expected: varargs[string]) =
|
proc doDiff(a, b: VNode; expected: varargs[string]) =
|
||||||
discard diff(b, a, nil, vnodeToDom(a, kxi), kxi)
|
diff(b, a, nil, vnodeToDom(a, kxi), kxi)
|
||||||
|
var j = 0
|
||||||
for i in 0..<kxi.patchLen:
|
for i in 0..<kxi.patchLen:
|
||||||
let p = $kxi.patches[i].k & " " & shortRepr(kxi.patches[i].n)
|
if kxi.patches[i].k != pkSame:
|
||||||
if i >= expected.len:
|
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
|
echo "patches differ; expected nothing but got: ", p
|
||||||
inc err
|
inc err
|
||||||
elif p != expected[i]:
|
elif p != expected[j]:
|
||||||
echo "patches differ; expected ", expected[i], " but got: ", p
|
echo "patches differ; expected ", expected[i], " but got: ", p
|
||||||
inc err
|
inc err
|
||||||
|
inc j
|
||||||
#hasDom(kxi.currentTree)
|
#hasDom(kxi.currentTree)
|
||||||
kxi.patchLen = 0
|
kxi.patchLen = 0
|
||||||
|
|
||||||
|
|
@ -156,16 +160,17 @@ proc testWild() =
|
||||||
let a = createEntries(entries)
|
let a = createEntries(entries)
|
||||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
||||||
let b = createEntries(entries)
|
let b = createEntries(entries)
|
||||||
doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 5", "pkRemove nil")
|
doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil")
|
||||||
|
|
||||||
proc testWildInsert() =
|
proc testWildInsert() =
|
||||||
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
|
||||||
let a = createEntries(entries)
|
let a = createEntries(entries)
|
||||||
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
|
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
|
||||||
let b = createEntries(entries)
|
let b = createEntries(entries)
|
||||||
doDiff(a, b, "pkInsertBefore button 7", "pkAppend div 5")
|
doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7")
|
||||||
|
|
||||||
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard)
|
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (data: RouterData): VNode = discard,
|
||||||
|
byId: newJDict[cstring, VNode]())
|
||||||
|
|
||||||
testAppend()
|
testAppend()
|
||||||
testInsert()
|
testInsert()
|
||||||
|
|
|
||||||
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
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 +1 @@
|
||||||
--path: "../src"
|
--path: "../karax"
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,9 @@ proc main =
|
||||||
exec("nim js examples/mediaplayer/playerapp.nim")
|
exec("nim js examples/mediaplayer/playerapp.nim")
|
||||||
exec("nim js examples/carousel/carousel.nim")
|
exec("nim js examples/carousel/carousel.nim")
|
||||||
exec("nim js -d:nodejs -r tests/difftest.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()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
## Simple tool to quickly run Karax applications. Generates the HTML
|
|
||||||
## required to run a Karax app and opens it in a browser.
|
|
||||||
|
|
||||||
import os, strutils, parseopt, browsers
|
|
||||||
|
|
||||||
const
|
|
||||||
html = """
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head><title>$1</title></head>
|
|
||||||
<body id="body">
|
|
||||||
<div id="ROOT" />
|
|
||||||
<script type="text/javascript" src="$1.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
proc exec(cmd: string) =
|
|
||||||
if os.execShellCmd(cmd) != 0:
|
|
||||||
quit "External command failed: " & cmd
|
|
||||||
|
|
||||||
proc main =
|
|
||||||
var op = initOptParser()
|
|
||||||
var rest = op.cmdLineRest
|
|
||||||
var file = ""
|
|
||||||
var run = false
|
|
||||||
while true:
|
|
||||||
op.next()
|
|
||||||
case op.kind
|
|
||||||
of cmdLongOption:
|
|
||||||
if op.key == "run":
|
|
||||||
run = true
|
|
||||||
rest = rest.replace("--run ")
|
|
||||||
of cmdShortOption:
|
|
||||||
if op.key == "r":
|
|
||||||
run = true
|
|
||||||
rest = rest.replace("-r ")
|
|
||||||
of cmdArgument: file = op.key
|
|
||||||
of cmdEnd: break
|
|
||||||
|
|
||||||
if file.len == 0: quit "filename expected"
|
|
||||||
let name = file.splitFile.name
|
|
||||||
createDir("nimcache")
|
|
||||||
exec("nim js --out:nimcache/" & name & ".js " & rest)
|
|
||||||
let dest = "nimcache" / name & ".html"
|
|
||||||
writeFile(dest, html % name)
|
|
||||||
if run: openDefaultBrowser(dest)
|
|
||||||
|
|
||||||
main()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue