Implements anchor awareness.

This commit is contained in:
Dominik Picheta 2018-05-15 22:13:53 +01:00
commit bc104ff41d
4 changed files with 107 additions and 79 deletions

View file

@ -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())
])

View file

@ -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}")
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}")

View file

@ -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"))

View file

@ -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``