clean todoapp example

This commit is contained in:
Andreas Rumpf 2017-04-05 00:44:58 +02:00
commit d22a1833cc
11 changed files with 713 additions and 95 deletions

View file

@ -1,8 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Todo app</title> <title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head> </head>
<body id="body"> <body id="body">

View file

@ -1,30 +1,22 @@
import vdom, karax, karaxdsl, jdict, jstrutils import vdom, karax, karaxdsl, jdict, jstrutils
when false: type
var plugins {.exportc.}: seq[(string, proc())] = @[] Filter = enum
all, active, completed
proc onInput(val: cstring) =
kout val
if val == "dyn":
let body = getElementById("body")
body.prepend(tree("script", [("type", "text/javascript"), ("src", "nimcache/dyn.js")]))
redraw()
kout(plugins.len)
if plugins.len > 0:
plugins[0][1]()
var var
entries: seq[cstring] entries: seq[(cstring, bool)]
selectedEntry = -1 selectedEntry = -1
filter: Filter
proc onTodoEnter(val: cstring) = proc onTodoEnter(ev: Event; n: VNode) =
entries.add val entries.add((n.value, false))
n.value = ""
proc removeHandler(ev: Event; n: VNode) = proc removeHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "remove:") let id = suffixAsInt(n.id, "remove:")
#entries.delete(id) entries[id] = (cstring(nil), false)
entries[id] = nil
proc editHandler(ev: Event; n: VNode) = proc editHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "edit:") let id = suffixAsInt(n.id, "edit:")
@ -33,57 +25,111 @@ proc editHandler(ev: Event; n: VNode) =
when defined(usecache): when defined(usecache):
var entryCache = newJDict[int, VNode]() var entryCache = newJDict[int, VNode]()
proc editInput(i: int; d: cstring): VNode = proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc onTodoChange(val: cstring) =
entries[i] = val
selectedEntry = -1
result = enterInput("todo-edit", d, onTodoChange)
result.setOnfocuslost(proc (ev: Event; n: VNode) = selectedEntry = -1)
proc createEntry(i: int; d: cstring; selected: bool): VNode = proc editEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "todo-edit:")
entries[id][0] = n.value
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "toggle:")
entries[id][1] = not entries[id][1]
proc onAllDone(ev: Event; n: VNode) =
entries = @[]
selectedEntry = -1
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entries.len:
if entries[i][1]: entries[i][0] = nil
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring"")
proc toVis(visible: bool): cstring =
(if visible: cstring"visible" else: "hidden")
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
# implement caching: # implement caching:
when defined(usecache): when defined(usecache):
if entryCache.contains(i): if entryCache.contains(i):
let old = entryCache[i] let old = entryCache[i]
return old return old
result = buildHtml(tr) do: result = buildHtml(tr):
td(id="edit:" & $i, onclick=editHandler): li(class=toClass(completed)):
if selected: if not selected:
editInput(i, d) tdiv(class = "view"):
else: input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, id="toggle:" & &id)
label(onDblClick=editHandler, id="edit:" & &id):
text d text d
td: button(class = "destroy", id="remove:" & $id, onclick=removeHandler)
span(id="remove:" & $i, onclick=removeHandler): else:
text "[remove]" input(class = "edit", name = "title", id = "todo-edit:" & &id,
onfocusLost = focusLost,
onenter = editEntry, value = d)
when defined(usecache): when defined(usecache):
entryCache[i] = result entryCache[i] = result
proc createDom(): VNode = proc createDom(): VNode =
result = buildHtml(tdiv) do: result = buildHtml(tdiv(class="todomvc-wrapper")):
tdiv(id = "sheader"): section(class = "todoapp"):
#text "plugin" header(class = "header"):
#realtimeInput("by-name", "", onInput) h1:
#br() text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
text cstring"todo" onenter = onTodoEnter)
enterInput("todo-input", "", onTodoEnter) section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
var entriesCount = 0 var entriesCount = 0
table(class = "wl"): var completedCount = 0
for i, d in entries: ul(class = "todo-list"):
if d != nil: for i, d in pairs(entries):
createEntry(i, d, i == selectedEntry) if d[0] != nil:
let b = case filter
of all: true
of active: not d[1]
of completed: d[1]
if b:
createEntry(i, d[0], d[1], i == selectedEntry)
inc completedCount, ord(d[1])
inc entriesCount inc entriesCount
tdiv(id = "footer"): footer(class = "footer"):
text cstring"Entries: " & &entriesCount span(class = "todo-count"):
proc onAllDone(ev: Event; n: VNode) = strong:
entries = @[] text(&entriesCount)
selectedEntry = -1 text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
button "All done!", onAllDone, entriesCount == 0 or selectedEntry >= 0 ul(class = "filters"):
li:
anchor(class = selected(all), href = "#/"):
text "All"
li:
anchor(class = selected(active), href = "#/active"):
text "Active"
li:
anchor(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
setOnHashChange(proc(hash: cstring) =
if hash == cstring"#/": filter = all
elif hash == cstring"#/completed": filter = completed
elif hash == cstring"#/active": filter = active
)
setRenderer createDom setRenderer createDom
proc onload(session: cstring) {.exportc.} = proc onload(session: cstring) {.exportc.} =
for i in 0..10_000: for i in 0..1: # 0_000:
entries.add("Entry " & $i) entries.add((cstring"Entry " & &i, false))
init() init()

View file

@ -9,3 +9,5 @@ proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".} 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 #[#]".}

View file

@ -2,6 +2,8 @@
proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.} proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, 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 substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.} proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
@ -11,3 +13,6 @@ proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nod
proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.} proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.}

View file

@ -42,6 +42,7 @@ proc vnodeToDom(n: VNode): Element =
if n.class != nil: if n.class != nil:
result.class = n.class result.class = n.class
for k, v in attrs(n): for k, v in attrs(n):
if v != nil:
result.setAttribute(k, v) result.setAttribute(k, v)
let myn = n let myn = n
for e, h in items(n.events): for e, h in items(n.events):
@ -90,9 +91,10 @@ proc replaceById(id: cstring; newTree: Node) =
proc equals(a, b: VNode): bool = proc equals(a, b: VNode): bool =
if a.kind != b.kind: return false if a.kind != b.kind: return false
if a.id != b.id: return false if a.id != b.id: return false
if a.kind == VNodeKind.text: #if a.kind == VNodeKind.text:
if a.text != b.text: return false if a.text != b.text: return false
if not sameAttrs(a, b): return false if not sameAttrs(a, b): return false
if a.class != b.class: return false
# XXX test event listeners here? # XXX test event listeners here?
# --> maybe give nodes a hash? # --> maybe give nodes a hash?
when false: when false:
@ -196,6 +198,9 @@ template onImpl(s) {.dirty.} =
proc setOnclick*(e: VNode; action: EventHandler) = proc setOnclick*(e: VNode; action: EventHandler) =
onImpl EventKind.onclick onImpl EventKind.onclick
proc setOnDblclick*(e: VNode; action: EventHandler) =
onImpl EventKind.onclick
proc setOnfocuslost*(e: VNode; action: EventHandler) = proc setOnfocuslost*(e: VNode; action: EventHandler) =
onImpl EventKind.onblur onImpl EventKind.onblur
@ -205,6 +210,14 @@ proc setOnchanged*(e: VNode; action: EventHandler) =
proc setOnscroll*(e: VNode; action: EventHandler) = proc setOnscroll*(e: VNode; action: EventHandler) =
onImpl EventKind.onscroll onImpl EventKind.onscroll
proc setOnHashChange*(action: proc (hashPart: cstring)) =
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
proc wrapper() =
action(hashPart)
redraw()
onhashchange = wrapper
var var
linkCounter: int linkCounter: int
@ -219,7 +232,8 @@ proc link*(action: EventHandler): VNode =
result.setAttr("href", "#") result.setAttr("href", "#")
result.setOnclick action result.setOnclick action
proc button*(caption: cstring; action: EventHandler; disabled=false): VNode = when false:
proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
result = newVNode(VNodeKind.button) result = newVNode(VNodeKind.button)
result.add text(caption) result.add text(caption)
if action != nil: if action != nil:
@ -299,29 +313,36 @@ proc tr*(kids: varargs[VNode]): VNode =
proc getAttr(e: Element; key: cstring): cstring {. proc getAttr(e: Element; key: cstring): cstring {.
importcpp: "#.getAttribute(#)", nodecl.} importcpp: "#.getAttribute(#)", nodecl.}
proc realtimeInput*(id, val: cstring; changed: proc(value: cstring)): VNode = template nativeValue(ev): cstring = cast[Element](ev.target).value
template setNativeValue(ev, val) = cast[Element](ev.target).value = val
proc realtimeInput*(val: cstring; changed: EventHandler): VNode =
#let oldElem = getElementById(id) #let oldElem = getElementById(id)
#if oldElem != nil: return oldElem #if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value #let newVal = if oldElem.isNil: val else: $oldElem.value
var timer: Timeout var timer: Timeout
proc wrapper() =
changed(document.getElementById(id).value)
redraw()
proc onkeyup(ev: Event; n: VNode) = proc onkeyup(ev: Event; n: VNode) =
proc wrapper() =
n.value = nativeValue(ev)
changed(ev, n)
setNativeValue(ev, n.value)
redraw()
if timer != nil: clearTimeout(timer) if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400) timer = setTimeout(wrapper, 400)
result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
result.id = id
result.value = val result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup) result.addEventListener(EventKind.onkeyup, onkeyup)
proc enterInput*(id, val: cstring; onenter: proc(value: cstring)): VNode = proc enterInput*(id, val: cstring; onenter: EventHandler): VNode =
#let oldElem = getElementById(id) #let oldElem = getElementById(id)
#if oldElem != nil: return oldElem #if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value #let newVal = if oldElem.isNil: val else: $oldElem.value
proc onkeyup(ev: Event; n: VNode) = proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13: if ev.keyCode == 13:
onenter(document.getElementById(id).value) n.value = nativeValue(ev)
onenter(ev, n)
setNativeValue(ev, n.value)
redraw() redraw()
result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
@ -329,6 +350,14 @@ proc enterInput*(id, val: cstring; onenter: proc(value: cstring)): VNode =
result.value = val result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup) result.addEventListener(EventKind.onkeyup, onkeyup)
proc setOnEnter*(n: VNode; onenter: EventHandler) =
proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13:
n.value = nativeValue(ev)
onenter(ev, n)
setNativeValue(ev, n.value)
redraw()
n.addEventListener(EventKind.onkeyup, onkeyup)
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring; data: cstring;
@ -366,7 +395,7 @@ proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)];
proc setupErrorHandler*(useAlert=false) = proc setupErrorHandler*(useAlert=false) =
## 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,
## the error message it put into the console, otherwise `alert` ## the error message is put into the console, otherwise `alert`
## is called. ## is called.
proc stackTraceAsCstring(): cstring = cstring(getStackTrace()) proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
{.emit: """ {.emit: """
@ -381,3 +410,17 @@ proc setupErrorHandler*(useAlert=false) =
};""".} };""".}
{.pop.} {.pop.}
when false:
var plugins {.exportc.}: seq[(string, proc())] = @[]
proc onInput(val: cstring) =
kout val
if val == "dyn":
let body = getElementById("body")
body.prepend(tree("script", [("type", "text/javascript"), ("src", "nimcache/dyn.js")]))
redraw()
kout(plugins.len)
if plugins.len > 0:
plugins[0][1]()

View file

@ -1,12 +1,8 @@
import macros, karax, vdom import macros, karax, vdom
from strutils import startsWith from strutils import startsWith, toLowerAscii
const const
SupportedTags* = ["tdiv", "div",
"table", "tr", "td", "th", "thead", "tbody",
"link", "span", "label", "br"
]
StmtContext = ["kout", "inc", "echo", "dec"] StmtContext = ["kout", "inc", "echo", "dec"]
proc getName(n: NimNode): string = proc getName(n: NimNode): string =
@ -23,11 +19,13 @@ proc getName(n: NimNode): string =
#echo repr n #echo repr n
expectKind(n, nnkIdent) expectKind(n, nnkIdent)
var gid {.compileTime.}: int
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)
proc isTag(s: string): bool =
for i in VNodeKind.low.succ..VNodeKind.high:
if $i == s: return true
proc tcall2(n, tmpContext: NimNode): NimNode = proc tcall2(n, tmpContext: NimNode): NimNode =
# we need to distinguish statement and expression contexts: # we need to distinguish statement and expression contexts:
# every call statement 's' needs to be transformed to 'dest.add s'. # every call statement 's' needs to be transformed to 'dest.add s'.
@ -62,11 +60,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result = n result = n
of nnkCallKinds: of nnkCallKinds:
let op = getName(n[0]) let op = getName(n[0])
let idx = find(SupportedTags, op) if isTag(op):
if idx >= 0: let tmp = genSym(nskLet, "tmp")
let tag = SupportedTags[idx]
inc gid
let tmp = genSym(nskLet, "tmp" & $gid)
result = newTree( result = newTree(
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])))) newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))))
@ -77,14 +72,14 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
let key = getName x[0] let key = getName x[0]
if key.startsWith("on"): if key.startsWith("on"):
result.add newCall(!("set" & key), tmp, x[1]) result.add newCall(!("set" & key), tmp, x[1])
elif key == "id" or key == "class": elif key == "id" or key == "class" or key == "value":
result.add newDotAsgn(tmp, key, x[1]) result.add newDotAsgn(tmp, key, x[1])
else: else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
else: else:
result.add tcall2(x, tmp) result.add tcall2(x, tmp)
if tag == "link": #if tag == "link":
result.add newCall(bindSym"setAttr", tmp, newLit"href", newLit"#") # result.add newCall(bindSym"setAttr", tmp, newLit"href", newLit"#")
if tmpContext == nil: if tmpContext == nil:
result.add tmp result.add tmp
else: else:
@ -98,7 +93,12 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
macro buildHtml*(tag, children: untyped): VNode = macro buildHtml*(tag, children: untyped): VNode =
expectKind children, nnkDo expectKind children, nnkDo
let call = newCall(tag, body(children)) var call: NimNode
if tag.kind in nnkCallKinds:
call = tag
else:
call = newCall(tag)
call.add body(children)
result = tcall2(call, nil) result = tcall2(call, nil)
when defined(debugKaraxDsl): when defined(debugKaraxDsl):
echo repr result echo repr result

View file

@ -5,5 +5,5 @@ Karax is a prototype for developing single page applications in Nim.
To try it out, run:: To try it out, run::
nim js example.nim nim js todoapp.nim
open example.html open todoapp.html

378
style.css Normal file
View file

@ -0,0 +1,378 @@
html,
body {
margin: 0;
padding: 0;
}
.todomvc-wrapper {
visibility: visible !important;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '>';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: 'x';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
/* font-size: 10px; */
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

19
todoapp.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/todoapp.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>
</body>
</html>

121
todoapp.nim Normal file
View file

@ -0,0 +1,121 @@
import vdom, karax, karaxdsl, jdict, jstrutils
type
Filter = enum
all, active, completed
var
entries: seq[(cstring, bool)]
selectedEntry = -1
filter: Filter
proc onTodoEnter(ev: Event; n: VNode) =
entries.add((n.value, false))
n.value = ""
proc removeHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "remove:")
entries[id] = (cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "edit:")
selectedEntry = id
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "todo-edit:")
entries[id][0] = n.value
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "toggle:")
entries[id][1] = not entries[id][1]
proc onAllDone(ev: Event; n: VNode) =
entries = @[]
selectedEntry = -1
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entries.len:
if entries[i][1]: entries[i][0] = nil
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring(nil))
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, id="toggle:" & &id)
label(onDblClick=editHandler, id="edit:" & &id):
text d
button(class = "destroy", id="remove:" & $id, onclick=removeHandler)
else:
input(class = "edit", name = "title", id = "todo-edit:" & &id,
onfocusLost = focusLost,
onenter = editEntry, value = d)
proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
header(class = "header"):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onenter = onTodoEnter)
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
var entriesCount = 0
var completedCount = 0
ul(class = "todo-list"):
for i, d in pairs(entries):
if d[0] != nil:
let b = case filter
of all: true
of active: not d[1]
of completed: d[1]
if b:
createEntry(i, d[0], d[1], i == selectedEntry)
inc completedCount, ord(d[1])
inc entriesCount
footer(class = "footer"):
span(class = "todo-count"):
strong:
text(&entriesCount)
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
anchor(class = selected(all), href = "#/"):
text "All"
li:
anchor(class = selected(active), href = "#/active"):
text "Active"
li:
anchor(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
setOnHashChange(proc(hash: cstring) =
if hash == cstring"#/": filter = all
elif hash == cstring"#/completed": filter = completed
elif hash == cstring"#/active": filter = active
)
setRenderer createDom
proc onload(session: cstring) {.exportc.} =
for i in 0..1: # 0_000:
entries.add((cstring"Entry " & &i, false))
init()

View file

@ -9,21 +9,24 @@ type
tdiv, tdiv,
table, tr, td, th, thead, tbody, table, tr, td, th, thead, tbody,
link, span, label, br, select, option, link, span, label, br, select, option,
fieldset, input, button fieldset, input, button, ul, li, section, header, footer,
h1, h2, h3, h4, h5, strong
const const
toTag*: array[VNodeKind, cstring] = [ toTag*: array[VNodeKind, cstring] = [
cstring"#text", "A", "DIV", "TABLE", "TR", "TD", "TH", "THEAD", "TBODY", "LINK", cstring"#text", "A", "DIV", "TABLE", "TR", "TD", "TH", "THEAD", "TBODY", "LINK",
"SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON" "SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON", "UL",
"LI", "SECTION", "HEADER", "FOOTER", "H1", "H2", "H3", "H4", "H5", "STRONG"
] ]
type type
EventKind* {.pure.} = enum EventKind* {.pure.} = enum
onclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll onclick, ondblclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll
const const
toEventName*: array[EventKind, cstring] = [ toEventName*: array[EventKind, cstring] = [
cstring"click", "keyup", "keydown", "keypressed", "blur", "change", "scroll" cstring"click", "dblclick", "keyup", "keydown", "keypressed", "blur",
"change", "scroll"
] ]
type type