diff --git a/example.html b/example.html index 9b2fb48..35b9837 100644 --- a/example.html +++ b/example.html @@ -1,8 +1,9 @@ - + Todo app + diff --git a/example.nim b/example.nim index c7d922b..0ea805e 100644 --- a/example.nim +++ b/example.nim @@ -1,30 +1,22 @@ import vdom, karax, karaxdsl, jdict, jstrutils -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]() +type + Filter = enum + all, active, completed var - entries: seq[cstring] + entries: seq[(cstring, bool)] selectedEntry = -1 + filter: Filter -proc onTodoEnter(val: cstring) = - entries.add val +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.delete(id) - entries[id] = nil + entries[id] = (cstring(nil), false) proc editHandler(ev: Event; n: VNode) = let id = suffixAsInt(n.id, "edit:") @@ -33,57 +25,111 @@ proc editHandler(ev: Event; n: VNode) = when defined(usecache): var entryCache = newJDict[int, VNode]() -proc editInput(i: int; d: cstring): VNode = - 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 focusLost(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..= 0 + 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..10_000: - entries.add("Entry " & $i) + for i in 0..1: # 0_000: + entries.add((cstring"Entry " & &i, false)) init() diff --git a/jdict.nim b/jdict.nim index 7d27183..0eecb58 100644 --- a/jdict.nim +++ b/jdict.nim @@ -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 del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".} + diff --git a/jstrutils.nim b/jstrutils.nim index 99dbcbb..b398537 100644 --- a/jstrutils.nim +++ b/jstrutils.nim @@ -2,6 +2,8 @@ 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 contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", 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 toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} +proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.} + +proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.} diff --git a/karax.nim b/karax.nim index efb9f43..587f2a7 100644 --- a/karax.nim +++ b/karax.nim @@ -42,7 +42,8 @@ proc vnodeToDom(n: VNode): Element = if n.class != nil: result.class = n.class for k, v in attrs(n): - result.setAttribute(k, v) + if v != nil: + result.setAttribute(k, v) let myn = n for e, h in items(n.events): proc wrapper(): proc (ev: Event) = @@ -90,9 +91,10 @@ proc replaceById(id: cstring; newTree: Node) = proc equals(a, b: VNode): bool = if a.kind != b.kind: return false if a.id != b.id: return false - if a.kind == VNodeKind.text: - if a.text != b.text: return false + #if a.kind == VNodeKind.text: + if a.text != b.text: return false if not sameAttrs(a, b): return false + if a.class != b.class: return false # XXX test event listeners here? # --> maybe give nodes a hash? when false: @@ -196,6 +198,9 @@ template onImpl(s) {.dirty.} = proc setOnclick*(e: VNode; action: EventHandler) = onImpl EventKind.onclick +proc setOnDblclick*(e: VNode; action: EventHandler) = + onImpl EventKind.onclick + proc setOnfocuslost*(e: VNode; action: EventHandler) = onImpl EventKind.onblur @@ -205,6 +210,14 @@ proc setOnchanged*(e: VNode; action: EventHandler) = proc setOnscroll*(e: VNode; action: EventHandler) = 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 linkCounter: int @@ -219,13 +232,14 @@ proc link*(action: EventHandler): VNode = result.setAttr("href", "#") result.setOnclick action -proc button*(caption: cstring; action: EventHandler; disabled=false): VNode = - result = newVNode(VNodeKind.button) - result.add text(caption) - if action != nil: - result.setOnClick action - if disabled: - result.setAttr("disabled", "true") +when false: + proc button*(caption: cstring; action: EventHandler; disabled=false): VNode = + result = newVNode(VNodeKind.button) + result.add text(caption) + if action != nil: + result.setOnClick action + if disabled: + result.setAttr("disabled", "true") proc select*(choices: openarray[cstring]): VNode = result = newVNode(VNodeKind.select) @@ -299,29 +313,36 @@ proc tr*(kids: varargs[VNode]): VNode = proc getAttr(e: Element; key: cstring): cstring {. 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) #if oldElem != nil: return oldElem #let newVal = if oldElem.isNil: val else: $oldElem.value var timer: Timeout - proc wrapper() = - changed(document.getElementById(id).value) - redraw() 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) timer = setTimeout(wrapper, 400) result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) - result.id = id result.value = val 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) #if oldElem != nil: return oldElem #let newVal = if oldElem.isNil: val else: $oldElem.value proc onkeyup(ev: Event; n: VNode) = if ev.keyCode == 13: - onenter(document.getElementById(id).value) + n.value = nativeValue(ev) + onenter(ev, n) + setNativeValue(ev, n.value) redraw() 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.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)]; data: cstring; @@ -366,7 +395,7 @@ proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)]; proc setupErrorHandler*(useAlert=false) = ## Installs an error handler that transforms native JS unhandled ## exceptions into Nim based stack traces. If `useAlert` is false, - ## the error message it put into the console, otherwise `alert` + ## the error message is put into the console, otherwise `alert` ## is called. proc stackTraceAsCstring(): cstring = cstring(getStackTrace()) {.emit: """ @@ -381,3 +410,17 @@ proc setupErrorHandler*(useAlert=false) = };""".} {.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]() + diff --git a/karaxdsl.nim b/karaxdsl.nim index 5afe3a2..e90a9b3 100644 --- a/karaxdsl.nim +++ b/karaxdsl.nim @@ -1,12 +1,8 @@ import macros, karax, vdom -from strutils import startsWith +from strutils import startsWith, toLowerAscii const - SupportedTags* = ["tdiv", "div", - "table", "tr", "td", "th", "thead", "tbody", - "link", "span", "label", "br" - ] StmtContext = ["kout", "inc", "echo", "dec"] proc getName(n: NimNode): string = @@ -23,11 +19,13 @@ proc getName(n: NimNode): string = #echo repr n expectKind(n, nnkIdent) -var gid {.compileTime.}: int - proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode = result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x) +proc isTag(s: string): bool = + for i in VNodeKind.low.succ..VNodeKind.high: + if $i == s: return true + proc tcall2(n, tmpContext: NimNode): NimNode = # we need to distinguish statement and expression contexts: # every call statement 's' needs to be transformed to 'dest.add s'. @@ -62,11 +60,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result = n of nnkCallKinds: let op = getName(n[0]) - let idx = find(SupportedTags, op) - if idx >= 0: - let tag = SupportedTags[idx] - inc gid - let tmp = genSym(nskLet, "tmp" & $gid) + if isTag(op): + let tmp = genSym(nskLet, "tmp") result = newTree( if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])))) @@ -77,14 +72,14 @@ proc tcall2(n, tmpContext: NimNode): NimNode = let key = getName x[0] if key.startsWith("on"): 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]) else: result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) else: result.add tcall2(x, tmp) - if tag == "link": - result.add newCall(bindSym"setAttr", tmp, newLit"href", newLit"#") + #if tag == "link": + # result.add newCall(bindSym"setAttr", tmp, newLit"href", newLit"#") if tmpContext == nil: result.add tmp else: @@ -98,7 +93,12 @@ proc tcall2(n, tmpContext: NimNode): NimNode = macro buildHtml*(tag, children: untyped): VNode = 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) when defined(debugKaraxDsl): echo repr result diff --git a/readme.rst b/readme.rst index a126f09..a5fd38a 100644 --- a/readme.rst +++ b/readme.rst @@ -5,5 +5,5 @@ Karax is a prototype for developing single page applications in Nim. To try it out, run:: - nim js example.nim - open example.html + nim js todoapp.nim + open todoapp.html diff --git a/style.css b/style.css new file mode 100644 index 0000000..ceaf090 --- /dev/null +++ b/style.css @@ -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,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml,'); +} + +.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; + } +} \ No newline at end of file diff --git a/todoapp.html b/todoapp.html new file mode 100644 index 0000000..8470a8e --- /dev/null +++ b/todoapp.html @@ -0,0 +1,19 @@ + + + + + Todo app + + + + + +
+ + + + + + diff --git a/todoapp.nim b/todoapp.nim new file mode 100644 index 0000000..40bc4bb --- /dev/null +++ b/todoapp.nim @@ -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..