diff --git a/redesign/forum.nim b/redesign/forum.nim index 8f216d4..1c08ec4 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -58,7 +58,14 @@ proc render(): VNode = ), r("/t/@id", (params: Params) => - (renderPostList(params["id"].parseInt(), isLoggedIn())) + ( + let postId = getInt(($state.url.hash).substr(1), 0); + renderPostList( + params["id"].parseInt(), + if postId == 0: none[int]() else: some[int](postId), + isLoggedIn() + ) + ) ), r("/", (params: Params) => renderThreadList()) ]) diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index e783b4d..eeb0e92 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -1,72 +1,91 @@ -import strutils, options, strformat +import strutils, options, strformat, parseutils import dom except window -include karax/prelude -import karax / [kdom] +proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. + noSideEffect.} = + ## parses `s` into an integer in the range `validRange`. If successful, + ## `value` is modified to contain the result. Otherwise no exception is + ## raised and `value` is not touched; this way a reasonable default value + ## won't be overwritten. + var x = value + try: + discard parseutils.parseInt(s, x, 0) + except OverflowError: + discard + if x in validRange: value = x -const appName = "/karax/" +proc getInt*(s: string, default = 0): int = + ## Safely parses an int and returns it. + result = default + parseInt(s, result, 0..1_000_000_000) -proc class*(classes: varargs[tuple[name: string, present: bool]], - defaultClasses: string = ""): string = - result = defaultClasses & " " - for class in classes: - if class.present: result.add(class.name & " ") +when defined(js): + include karax/prelude + import karax / [kdom] -proc makeUri*(relative: string, appName=appName, includeHash=false): string = - ## Concatenates ``relative`` to the current URL in a way that is - ## (possibly) sane. - var relative = relative - assert appName in $window.location.pathname - if relative[0] == '/': relative = relative[1..^1] + const appName = "/karax/" - return $window.location.protocol & "//" & - $window.location.host & - appName & - relative & - $window.location.search & - (if includeHash: $window.location.hash else: "") + proc class*(classes: varargs[tuple[name: string, present: bool]], + defaultClasses: string = ""): string = + result = defaultClasses & " " + for class in classes: + if class.present: result.add(class.name & " ") -proc makeUri*(relative: string, params: varargs[(string, string)], - appName=appName, includeHash=false): string = - var query = "" - for i in 0 ..< params.len: - let param = params[i] - if i != 0: query.add("&") - query.add(param[0] & "=" & param[1]) + proc makeUri*(relative: string, appName=appName, includeHash=false): string = + ## Concatenates ``relative`` to the current URL in a way that is + ## (possibly) sane. + var relative = relative + assert appName in $window.location.pathname + if relative[0] == '/': relative = relative[1..^1] - if query.len > 0: - makeUri(relative & "?" & query, appName) - else: - makeUri(relative, appName) + return $window.location.protocol & "//" & + $window.location.host & + appName & + relative & + $window.location.search & + (if includeHash: $window.location.hash else: "") -proc navigateTo*(uri: cstring) = - # TODO: This was annoying. Karax also shouldn't have its own `window`. - dom.pushState(dom.window.history, 0, cstring"", uri) + proc makeUri*(relative: string, params: varargs[(string, string)], + appName=appName, includeHash=false): string = + var query = "" + for i in 0 ..< params.len: + let param = params[i] + if i != 0: query.add("&") + query.add(param[0] & "=" & param[1]) - # Fire the popState event. - dom.window.dispatchEvent(newEvent("popstate")) + if query.len > 0: + makeUri(relative & "?" & query, appName) + else: + makeUri(relative, appName) -proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? - e.preventDefault() + proc navigateTo*(uri: cstring) = + # TODO: This was annoying. Karax also shouldn't have its own `window`. + dom.pushState(dom.window.history, 0, cstring"", uri) - # TODO: Why does Karax have it's own Node type? That's just silly. - let url = cast[dom.Node](n.dom).getAttribute(cstring"href") + # Fire the popState event. + dom.window.dispatchEvent(newEvent("popstate")) - navigateTo(url) + proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? + e.preventDefault() -type - FormData* = ref object -proc newFormData*(): FormData - {.importcpp: "new FormData()", constructor.} -proc newFormData*(form: dom.Element): FormData - {.importcpp: "new FormData(@)", constructor.} -proc get*(form: FormData, key: cstring): cstring - {.importcpp: "#.get(@)".} -proc append*(form: FormData, key, val: cstring) - {.importcpp: "#.append(@)".} + # TODO: Why does Karax have it's own Node type? That's just silly. + let url = cast[dom.Node](n.dom).getAttribute(cstring"href") -proc renderProfileUrl*(username: string): string = - makeUri(fmt"/profile/{username}") + navigateTo(url) -proc renderPostUrl*(threadId, postId: int): string = - makeUri(fmt"/t/{threadId}#{postId}") \ No newline at end of file + type + FormData* = ref object + proc newFormData*(): FormData + {.importcpp: "new FormData()", constructor.} + proc newFormData*(form: dom.Element): FormData + {.importcpp: "new FormData(@)", constructor.} + proc get*(form: FormData, key: cstring): cstring + {.importcpp: "#.get(@)".} + proc append*(form: FormData, key, val: cstring) + {.importcpp: "#.append(@)".} + + proc renderProfileUrl*(username: string): string = + makeUri(fmt"/profile/{username}") + + proc renderPostUrl*(threadId, postId: int): string = + makeUri(fmt"/t/{threadId}#{postId}") \ No newline at end of file diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 314ddfa..ee41d12 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -12,6 +12,8 @@ type posts*: seq[Post] when defined(js): + from dom import nil + include karax/prelude import karax / [vstyles, kajax, kdom] @@ -38,7 +40,7 @@ when defined(js): var state = newState() - proc onPostList(httpStatus: int, response: kstring) = + proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) = state.loading = false state.status = httpStatus.HttpCode if state.status != Http200: return @@ -48,6 +50,18 @@ when defined(js): state.list = some(list) + # The anchor should be jumped to once all the posts have been loaded. + if postId.isSome(): + discard setTimeout( + () => ( + # Would have used scrollIntoView but then the `:target` selector + # isn't activated. + window.location.hash = ""; + window.location.hash = "#" & $postId.get() + ), + 100 + ) + proc onMorePosts(httpStatus: int, response: kstring, start: int) = state.loading = false state.status = httpStatus.HttpCode @@ -190,13 +204,17 @@ when defined(js): tdiv(class="information-title"): text diffStr - proc renderPostList*(threadId: int, isLoggedIn: bool): VNode = + proc renderPostList*(threadId: int, postId: Option[int], + isLoggedIn: bool): VNode = if state.status != Http200: return renderError("Couldn't retrieve posts.") if state.list.isNone or state.list.get().thread.id != threadId: - let uri = makeUri("posts.json", ("id", $threadId)) - ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r)) + var params = @[("id", $threadId)] + if postId.isSome(): + params.add(("anchor", $postId.get())) + let uri = makeUri("posts.json", params) + ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r, postId)) return buildHtml(tdiv(class="loading loading-lg")) diff --git a/utils.nim b/utils.nim index 8f4d9a8..2d6e97e 100644 --- a/utils.nim +++ b/utils.nim @@ -7,24 +7,8 @@ from times import getTime, getGMTime, format let UsernameIdent* = IdentChars # TODO: Double check that everyone follows this. - -proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. - noSideEffect.} = - ## parses `s` into an integer in the range `validRange`. If successful, - ## `value` is modified to contain the result. Otherwise no exception is - ## raised and `value` is not touched; this way a reasonable default value - ## won't be overwritten. - var x = value - try: - discard parseutils.parseInt(s, x, 0) - except OverflowError: - discard - if x in validRange: value = x - -proc getInt*(s: string, default = 0): int = - ## Safely parses an int and returns it. - result = default - parseInt(s, result, 0..1_000_000_000) +import redesign/karaxutils +export parseInt proc `%`*[T](opt: Option[T]): JsonNode = ## Generic constructor for JSON data. Creates a new ``JNull JsonNode``