Implements anchor awareness.
This commit is contained in:
parent
bd150d04de
commit
bc104ff41d
4 changed files with 107 additions and 79 deletions
|
|
@ -58,7 +58,14 @@ proc render(): VNode =
|
||||||
),
|
),
|
||||||
r("/t/@id",
|
r("/t/@id",
|
||||||
(params: Params) =>
|
(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())
|
r("/", (params: Params) => renderThreadList())
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,91 @@
|
||||||
import strutils, options, strformat
|
import strutils, options, strformat, parseutils
|
||||||
import dom except window
|
import dom except window
|
||||||
|
|
||||||
include karax/prelude
|
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
||||||
import karax / [kdom]
|
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]],
|
when defined(js):
|
||||||
defaultClasses: string = ""): string =
|
include karax/prelude
|
||||||
result = defaultClasses & " "
|
import karax / [kdom]
|
||||||
for class in classes:
|
|
||||||
if class.present: result.add(class.name & " ")
|
|
||||||
|
|
||||||
proc makeUri*(relative: string, appName=appName, includeHash=false): string =
|
const appName = "/karax/"
|
||||||
## 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]
|
|
||||||
|
|
||||||
return $window.location.protocol & "//" &
|
proc class*(classes: varargs[tuple[name: string, present: bool]],
|
||||||
$window.location.host &
|
defaultClasses: string = ""): string =
|
||||||
appName &
|
result = defaultClasses & " "
|
||||||
relative &
|
for class in classes:
|
||||||
$window.location.search &
|
if class.present: result.add(class.name & " ")
|
||||||
(if includeHash: $window.location.hash else: "")
|
|
||||||
|
|
||||||
proc makeUri*(relative: string, params: varargs[(string, string)],
|
proc makeUri*(relative: string, appName=appName, includeHash=false): string =
|
||||||
appName=appName, includeHash=false): string =
|
## Concatenates ``relative`` to the current URL in a way that is
|
||||||
var query = ""
|
## (possibly) sane.
|
||||||
for i in 0 ..< params.len:
|
var relative = relative
|
||||||
let param = params[i]
|
assert appName in $window.location.pathname
|
||||||
if i != 0: query.add("&")
|
if relative[0] == '/': relative = relative[1..^1]
|
||||||
query.add(param[0] & "=" & param[1])
|
|
||||||
|
|
||||||
if query.len > 0:
|
return $window.location.protocol & "//" &
|
||||||
makeUri(relative & "?" & query, appName)
|
$window.location.host &
|
||||||
else:
|
appName &
|
||||||
makeUri(relative, appName)
|
relative &
|
||||||
|
$window.location.search &
|
||||||
|
(if includeHash: $window.location.hash else: "")
|
||||||
|
|
||||||
proc navigateTo*(uri: cstring) =
|
proc makeUri*(relative: string, params: varargs[(string, string)],
|
||||||
# TODO: This was annoying. Karax also shouldn't have its own `window`.
|
appName=appName, includeHash=false): string =
|
||||||
dom.pushState(dom.window.history, 0, cstring"", uri)
|
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.
|
if query.len > 0:
|
||||||
dom.window.dispatchEvent(newEvent("popstate"))
|
makeUri(relative & "?" & query, appName)
|
||||||
|
else:
|
||||||
|
makeUri(relative, appName)
|
||||||
|
|
||||||
proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
|
proc navigateTo*(uri: cstring) =
|
||||||
e.preventDefault()
|
# 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.
|
# Fire the popState event.
|
||||||
let url = cast[dom.Node](n.dom).getAttribute(cstring"href")
|
dom.window.dispatchEvent(newEvent("popstate"))
|
||||||
|
|
||||||
navigateTo(url)
|
proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
type
|
# TODO: Why does Karax have it's own Node type? That's just silly.
|
||||||
FormData* = ref object
|
let url = cast[dom.Node](n.dom).getAttribute(cstring"href")
|
||||||
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 =
|
navigateTo(url)
|
||||||
makeUri(fmt"/profile/{username}")
|
|
||||||
|
|
||||||
proc renderPostUrl*(threadId, postId: int): string =
|
type
|
||||||
makeUri(fmt"/t/{threadId}#{postId}")
|
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}")
|
||||||
|
|
@ -12,6 +12,8 @@ type
|
||||||
posts*: seq[Post]
|
posts*: seq[Post]
|
||||||
|
|
||||||
when defined(js):
|
when defined(js):
|
||||||
|
from dom import nil
|
||||||
|
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax / [vstyles, kajax, kdom]
|
import karax / [vstyles, kajax, kdom]
|
||||||
|
|
||||||
|
|
@ -38,7 +40,7 @@ when defined(js):
|
||||||
var
|
var
|
||||||
state = newState()
|
state = newState()
|
||||||
|
|
||||||
proc onPostList(httpStatus: int, response: kstring) =
|
proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) =
|
||||||
state.loading = false
|
state.loading = false
|
||||||
state.status = httpStatus.HttpCode
|
state.status = httpStatus.HttpCode
|
||||||
if state.status != Http200: return
|
if state.status != Http200: return
|
||||||
|
|
@ -48,6 +50,18 @@ when defined(js):
|
||||||
|
|
||||||
state.list = some(list)
|
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) =
|
proc onMorePosts(httpStatus: int, response: kstring, start: int) =
|
||||||
state.loading = false
|
state.loading = false
|
||||||
state.status = httpStatus.HttpCode
|
state.status = httpStatus.HttpCode
|
||||||
|
|
@ -190,13 +204,17 @@ when defined(js):
|
||||||
tdiv(class="information-title"):
|
tdiv(class="information-title"):
|
||||||
text diffStr
|
text diffStr
|
||||||
|
|
||||||
proc renderPostList*(threadId: int, isLoggedIn: bool): VNode =
|
proc renderPostList*(threadId: int, postId: Option[int],
|
||||||
|
isLoggedIn: bool): VNode =
|
||||||
if state.status != Http200:
|
if state.status != Http200:
|
||||||
return renderError("Couldn't retrieve posts.")
|
return renderError("Couldn't retrieve posts.")
|
||||||
|
|
||||||
if state.list.isNone or state.list.get().thread.id != threadId:
|
if state.list.isNone or state.list.get().thread.id != threadId:
|
||||||
let uri = makeUri("posts.json", ("id", $threadId))
|
var params = @[("id", $threadId)]
|
||||||
ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r))
|
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"))
|
return buildHtml(tdiv(class="loading loading-lg"))
|
||||||
|
|
||||||
|
|
|
||||||
20
utils.nim
20
utils.nim
|
|
@ -7,24 +7,8 @@ from times import getTime, getGMTime, format
|
||||||
let
|
let
|
||||||
UsernameIdent* = IdentChars # TODO: Double check that everyone follows this.
|
UsernameIdent* = IdentChars # TODO: Double check that everyone follows this.
|
||||||
|
|
||||||
|
import redesign/karaxutils
|
||||||
proc parseInt*(s: string, value: var int, validRange: Slice[int]) {.
|
export parseInt
|
||||||
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)
|
|
||||||
|
|
||||||
proc `%`*[T](opt: Option[T]): JsonNode =
|
proc `%`*[T](opt: Option[T]): JsonNode =
|
||||||
## Generic constructor for JSON data. Creates a new ``JNull JsonNode``
|
## Generic constructor for JSON data. Creates a new ``JNull JsonNode``
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue