diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 3f970a8..7bc9c65 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -15,7 +15,7 @@ proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} proc mplayer*(id, resource: cstring): VNode {.compact.} = proc handler(ev: Event; n: VNode) = let myVideo = document.getElementById(id) - case n.key + case n.index of Play: if myVideo.paused: myVideo.play() @@ -27,13 +27,13 @@ proc mplayer*(id, resource: cstring): VNode {.compact.} = else: discard result = buildHtml(tdiv): - button(onclick=handler, key=Play): + button(onclick=handler, index=Play): text "Play/Pause" - button(onclick=handler, key=Big): + button(onclick=handler, index=Big): text "Big" - button(onclick=handler, key=Small): + button(onclick=handler, index=Small): text "Small" - button(onclick=handler, key=Normal): + button(onclick=handler, index=Normal): text "Normal" br() br() diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 0caa70c..47ddeae 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -45,19 +45,19 @@ proc onTodoEnter(ev: Event; n: VNode) = n.value = "" proc removeHandler(ev: Event; n: VNode) = - updateEntry(n.key, cstring(nil), false) + updateEntry(n.index, cstring(nil), false) proc editHandler(ev: Event; n: VNode) = - selectedEntry = n.key + selectedEntry = n.index proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 proc editEntry(ev: Event; n: VNode) = - setEntryContent(n.key, n.value) + setEntryContent(n.index, n.value) selectedEntry = -1 proc toggleEntry(ev: Event; n: VNode) = - let id = n.key + let id = n.index markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = @@ -83,12 +83,12 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compac if not selected: tdiv(class = "view"): input(class = "toggle", `type` = "checkbox", checked = toChecked(completed), - onclick=toggleEntry, key=id) - label(onDblClick=editHandler, key=id): + onclick=toggleEntry, index=id) + label(onDblClick=editHandler, index=id): text d - button(class = "destroy", key=id, onclick=removeHandler) + button(class = "destroy", index=id, onclick=removeHandler) else: - input(class = "edit", name = "title", key=id, + input(class = "edit", name = "title", index=id, onblur = focusLost, onkeyupenter = editEntry, value = d, setFocus=true) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim new file mode 100644 index 0000000..56330d0 --- /dev/null +++ b/experiments/nextgen.nim @@ -0,0 +1,120 @@ + +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive + +proc textInput*(text: RString; focus: RBool): VNode {.track.} = + proc onFlip(ev: Event; target: VNode) = + focus <- not focus.value + + proc onKeyupEnter(ev: Event; target: VNode) = + text <- target.value + + proc onkeyup(ev: Event; n: VNode) = + # keep displayValue up to date, but do not tell the client yet! + text.value = n.value + + result = buildHtml(input(`type`="text", + value=text.value, onblur=onFlip, onfocus=onFlip, + onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value)) + +var + errmsg = rstr("") + +makeReactive: + type + User = ref object + firstname, lastname: cstring + selected: bool + +var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")), + (User(rawFirstname: "Some", rawLastname: "One")), + (User(rawFirstname: "Some", rawLastname: "Two"))]) +var prevSelected: User = nil #newReactive[User](nil) + +proc unselect() = + if prevSelected != nil: + prevSelected.selected = false + prevSelected = nil + +proc select(u: User) = + unselect() + u.selected = true + prevSelected = u + +proc toUI*(isFirstname: bool): RString = + result = RString() + result.subscribe proc (v: cstring) = + if v.len > 0: + let p = prevSelected #selected.value + if p != nil: + if isFirstName: + p.firstname = v + else: + p.lastname = v + unselect() + errmsg <- "" + else: + errmsg <- "name must not be empty" + +var inpFirstname = toUI(true) +var inpLastname = toUI(false) + +proc adaptFocus(def = false): RBool = + result = RBool() + result.value = def + when false: + result.subscribe proc (hasFocus: bool) = + if not hasFocus: + unselect() + +var focusA = adaptFocus() +var focusB = adaptFocus() + +proc styler(): VStyle = + result = style( + (StyleAttr.position, cstring"relative"), + (StyleAttr.paddingLeft, cstring"10px"), + (StyleAttr.paddingRight, cstring"5px"), + (StyleAttr.height, cstring"30px"), + (StyleAttr.lineHeight, cstring"30px"), + (StyleAttr.border, cstring"solid 8px " & (if focusA.value: cstring"red" else: cstring"black")), + (StyleAttr.fontSize, cstring"12px"), + (StyleAttr.fontWeight, cstring"600") + ) + +var clicks = 0 + +proc renderUser(u: User): VNode {.track.} = + result = buildHtml(tdiv): + if u.selected: + inpFirstname <- u.firstname + tdiv: + textInput inpFirstname, focusA + inpLastname <- u.lastname + tdiv: + textInput inpLastname, focusB + else: + button: + text "..." + proc onclick(ev: Event; n: VNode) = + select(u) + text u.firstname & " " & u.lastname + button: + text "(x)" + proc onclick(ev: Event; n: VNode) = + gu.deleteElem(u) + +proc main(gu: RSeq[User]): VNode = + result = buildHtml(tdiv): + tdiv: + button: + text "Add User" + proc onclick(ev: Event; n: VNode) = + inc clicks + gu.add User(rawFirstname: "Added", rawLastname: &clicks) + tdiv: + text errmsg + vmapIt(gu, tdiv, renderUser(it)) + +proc init(): VNode = main(gu) + +setInitializer(init) diff --git a/experiments/scrollapp.nims b/experiments/scrollapp.nims deleted file mode 100644 index 532b206..0000000 --- a/experiments/scrollapp.nims +++ /dev/null @@ -1 +0,0 @@ ---path: "../src" diff --git a/src/i18n.nim b/src/i18n.nim new file mode 100644 index 0000000..42274ff --- /dev/null +++ b/src/i18n.nim @@ -0,0 +1,152 @@ +## Karax -- Single page applications for Nim. + +## i18n support for Karax applications. We distinguish between: +## +## - translate: Lookup text translations +## - localize: Localize Date and Time objects to local formats +## +## Localization has not yet been implemented. + +import languages, jdict, jstrutils + +var currentLanguage = Language.enUS + +proc getLanguage(): cstring = + {.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".} + +proc detectLanguage*(): Language = + let x = getLanguage() + for i in low(Language)..high(Language): + if languageToCode[i] == x: return i + return Language.enUS + +proc setCurrentLanguage*(newLanguage = detectLanguage()) = + currentLanguage = newLanguage + +proc getCurrentLanguage*(): Language = currentLanguage + +type Translation* = JDict[cstring, cstring] + +var + translations: array[Language, Translation] + +proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t + +proc addTranslation*(lang: Language; key, val: cstring) = + if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]() + translations[lang][key] = val + +proc translate(x: cstring): cstring = + let y = translations[currentLanguage] + if y != nil and y.contains(x): + result = y[x] + else: + result = x + +type TranslatedString* = distinct cstring + +template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x)) + +proc raiseInvalidFormat(errmsg: string) = + raise newException(ValueError, errmsg) + +discard """ + $[1:item|items]1$ selected. +""" + +proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".} +proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".} + +proc parseChoice(f: cstring; i, choice: int, + r: JSeq[char]) = + var i = i + while i < f.len: + var n = 0 + let oldI = i + var toAdd = false + while i < f.len and f{i} >= '0' and f{i} <= '9': + n = n * 10 + ord(f{i}) - ord('0') + inc i + if oldI != i: + if f{i} == ':': + inc i + else: + raiseInvalidFormat"':' after number expected" + toAdd = choice == n + else: + # an else section does not start with a number: + toAdd = true + while i < f.len and f{i} != ']' and f{i} != '|': + if toAdd: r.sadd f{i} + inc i + if toAdd: break + inc i + +proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".} +proc add(x: JSeq[char]; y: cstring) = + for i in 0..= '0' and f{i} <= '9': + j = j * 10 + ord(f{i}) - ord('0') + inc i + let idx = if not negative: j-1 else: args.len-j + r.add args[idx] + of '$': + inc(i) + r.add '$' + of '[': + let start = i+1 + while i < f.len and f{i} != ']': inc i + inc i + if i >= f.len: raiseInvalidFormat"']' expected" + case f{i} + of '#': + parseChoice(f, start, parseInt args[num], r) + inc i + inc num + of '1'..'9', '-': + var j = 0 + var negative = f{i} == '-' + if negative: inc i + while f{i} >= '0' and f{i} <= '9': + j = j * 10 + ord(f{i}) - ord('0') + inc i + let idx = if not negative: j-1 else: args.len-j + parseChoice(f, start, parseInt args[idx], r) + else: raiseInvalidFormat"argument index expected after ']'" + else: + raiseInvalidFormat("'#', '$', or number expected") + if i < f.len and f{i} == '$': inc i + else: + r.sadd f{i} + inc i + result = join(r) + + +when isMainModule: + addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1") + setCurrentLanguage(Language.deDE) + + echo(i18n"$#$ -> $#$" % [cstring"1", "2"]) + echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"]) + echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"]) + echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"]) + echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"]) + echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"]) diff --git a/src/kajax.nim b/src/kajax.nim index 414b9d2..c226c50 100644 --- a/src/kajax.nim +++ b/src/kajax.nim @@ -23,7 +23,7 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; 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(@)",} + proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".} var this {.importc: "this".}: ThisObj let ajax = newRequest() diff --git a/src/karax.nim b/src/karax.nim index 5ebb7dc..737f3ef 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -7,13 +7,6 @@ export kdom.Event proc kout*[T](x: T) {.importc: "console.log", varargs.} ## the preferred way of debugging karax applications. -proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".} -proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.} -proc key*(e: Node): VKey = - if e.hasProp"karaxKey": result = e.rawkey - else: result = -1 -proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} - type PatchKind = enum pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach @@ -24,6 +17,9 @@ type PatchV = object parent, newChild: VNode pos: int + ComponentPair = object + oldNode, newNode: VComponent + parent, current: Node type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have @@ -40,6 +36,8 @@ type patchesV: seq[PatchV] patchLenV: int runCount: int + components: seq[ComponentPair] + surpressRedraws*: bool when defined(stats): recursion: int @@ -64,6 +62,7 @@ template setNativeValue(ev, val) = cast[Element](ev.target).value = val template keyeventBody() = let v = nativeValue(ev) n.value = v + assert action != nil action(ev, n) if n.value != v: setNativeValue(ev, n.value) @@ -76,7 +75,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; let action = action let n = n result = proc (ev: Event) = - if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input: + if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input or n.kind == VNodeKind.select: keyeventBody() else: action(ev, n) @@ -119,7 +118,7 @@ proc applyEvents(n: VNode; kxi: KaraxInstance) = for i in 0..= parent.len: + kxi.addPatch(pkAppend, current, nil, kid) + parent.add(kid) + else: + let before = current.childNodes[position] + kxi.addPatch(pkInsertBefore, current, before, kid) + parent.insert(kid, position) + applyPatch(kxi) + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + +proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = + let olddom = oldNode.dom + doAssert olddom != nil + discard diff(newNode, oldNode, nil, olddom, kxi) + # this is a bit nasty: Since we cannot patch the 'parent' of + # the current VNode (because we don't store it at all!), we + # need to override the fields individually: + takeOverFields(newNode, oldNode) + applyComponents(kxi) + applyPatch(kxi) + if kxi.currentTree == oldNode: + kxi.currentTree = newNode + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return let newtree = kxi.renderer() @@ -440,16 +557,23 @@ proc dodraw(kxi: KaraxInstance) = newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: - kxi.currentTree = newtree - let asdom = vnodeToDom(kxi.currentTree, kxi) + let asdom = vnodeToDom(newtree, kxi) replaceById(kxi.rootId, asdom) else: doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) let olddom = document.getElementById(kxi.rootId) discard diff(newtree, kxi.currentTree, nil, olddom, kxi) #kout cstring"patch len ", patches.len - apply(kxi) - kxi.currentTree = newtree + when defined(profileKarax): + echo "<<<<<<<<<<<<<<" + echa reasons + applyComponents(kxi) + when defined(profileKarax): + echo "--------------" + echa reasons + echo ">>>>>>>>>>>>>>" + applyPatch(kxi) + kxi.currentTree = newtree doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: @@ -490,7 +614,20 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, patches: newSeq[Patch](60), - patchesV: newSeq[PatchV](30)) + patchesV: newSeq[PatchV](30), + components: @[]) + kxi = result + window.onload = init + +proc setInitializer*(renderer: proc (): VNode, root: cstring = "ROOT", + clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = + ## Setup Karax. Usually the return value can be ignored. + result = KaraxInstance(rootId: root, renderer: renderer, + postRenderCallback: clientPostRenderCallback, + patches: newSeq[Patch](60), + patchesV: newSeq[PatchV](30), + components: @[], + surpressRedraws: true) kxi = result window.onload = init @@ -503,7 +640,19 @@ proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler; ## a ``redraw``. proc wrapper(ev: Event; n: VNode) = action(ev, n) - redraw(kxi) + if not kxi.surpressRedraws: redraw(kxi) + addEventListener(n, k, wrapper) + +proc addEventHandler*(n: VNode; k: EventKind; action: proc(); + kxi: KaraxInstance = kxi) = + ## Implements the foundation of Karax's event management. + ## Karax DSL transforms ``tag(onEvent = handler)`` to + ## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)`` + ## where ``wrapper`` calls the passed ``action`` and then triggers + ## a ``redraw``. + proc wrapper(ev: Event; n: VNode) = + action() + if not kxi.surpressRedraws: redraw(kxi) addEventListener(n, k, wrapper) proc setOnHashChange*(action: proc (hashPart: cstring)) = diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 15c6fc6..dd480e1 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key", "style"] + SpecialAttrs = ["id", "class", "value", "index", "style"] proc getName(n: NimNode): string = case n.kind @@ -59,9 +59,22 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result.add n[0] for i in 1 ..< n.len: result.add tcall2(n[i], tmpContext) + of nnkProcDef: + let name = getName n[0] + if name.startsWith"on": + # turn it into an anon proc: + let anon = copyNimTree(n) + anon[0] = newEmptyNode() + if tmpContext == nil: + error "no VNode to attach the event handler to" + else: + result = newCall(bindSym"addEventHandler", tmpContext, + newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) + else: + result = n of nnkVarSection, nnkLetSection, nnkConstSection: result = n - of nnkCallKinds: + of nnkCallKinds - {nnkInfix}: let op = getName(n[0]) let ck = isComponent(op) if ck != ComponentKind.None: @@ -87,6 +100,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result.add newDotAsgn(tmp, key, x[1]) elif eqIdent(key, "setFocus"): result.add newCall(key, tmp, x[1], ident"kxi") + elif eqIdent(key, "events"): + result.add newCall(bindSym"mergeEvents", tmp, x[1]) else: result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) elif ck != ComponentKind.Tag: @@ -100,7 +115,28 @@ proc tcall2(n, tmpContext: NimNode): NimNode = else: result.add newCall(bindSym"add", tmpContext, tmp) elif tmpContext != nil and op notin StmtContext: - result = newCall(bindSym"add", tmpContext, n) + var hasEventHandlers = false + for i in 1..= 0: + x.s.delete(position) + x.broadcast(Mark) + x.broadcast(Deleted, position) + +when false: + proc `:=`[T](x: Reactive[T], f: proc(): T) = + toTrack = wrapObserver(f()) + x.value = f() + toTrack = nil + + proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = + let xl = x.L.value + let res = newRSeq[U](xl) + for i in 0..