import * as React from "react" import * as ReactDOM from "react-dom" import * as Immutable from "immutable" import * as i18next from 'i18next' import * as Option from "./option" import * as Slugify from "slugify" import {C, Mode, Cont, CmdCommon, Context, make_C, unit, bind} from './core' import {button} from './html' export type UrlElement = string | { kind:"int", name:K } export type UrlTemplate = Array> export let parse_url = function(template:UrlTemplate) : ((url:string) => Option.Option) { return url => { let res : any = {} let url_items = url.split("/") if (url_items.length != template.length) return Option.none() for (var i = 0; i < url_items.length; i++) { let x = Slugify(url_items[i]) let y = Slugify(template[i]) if (typeof y === "string") { if (x != y) return Option.none() } else { let n = parseInt(x) if (isNaN(n)) return Option.none() res[y.name] = n } } return Option.some(res as T) } } export let instantiate_url = function(template:UrlTemplate) : ((t:T) => string) { return t => { let url = "" for (var i = 0; i < template.length; i++) { let el = Slugify(template[i]) if (typeof el === "string") { url = i == 0 ? el : `${url}/${el}` } else { url = i == 0 ? `${t[el.name]}` : `${url}/${t[el.name]}` } } return url } } export type Url = PartialRetraction export let make_url = function(template:UrlTemplate) : Url { return { in:parse_url(template), out:instantiate_url(template) } } export let fallback_url = function() : Url<{}> { return { in:_ => Option.some({}), out:_ => "" } } export type PartialRetraction = { in:(_:A)=>Option.Option, out:(_:B)=>A } export type Route = { url:Url, page:(_:A)=>C } export type ApplicationProps = { mode:Mode, base_url:string, slug:string, routes:() => Promise>> } export type ApplicationState = { kind:"loading routes" } | { kind:"running", context:Context, routes:Immutable.List> } export class Application extends React.Component { constructor(props:ApplicationProps, context:any) { super(props, context) this.state = { kind:"loading routes" } } load() { this.props.routes().then(raw_routes => { let routes = Immutable.List>(raw_routes) let initial_page:C = undefined routes.forEach(r => { let p = r.url.in(this.props.slug).map(r.page) if (p.kind == "some") { initial_page = p.value return false } return true }) let newState: ApplicationState = {...this.state, kind:"running", context: this.context_from_props(this.props, initial_page), routes:routes } this.setState(newState) }).catch(() => setTimeout(() => this.load(), 250)) } componentDidMount() { this.load() let self = this let load = () => { if (self.state.kind != "running") return if (self.history.count() == 1) { let slug = self.history.peek() window.history.pushState(`${self.props.base_url}${slug}`, `${self.props.base_url}${slug}`, `${self.props.base_url}${slug}`) return } self.history = self.history.pop() let slug = self.history.peek() // console.log("back to", slug, old_history.toArray(), self.history.toArray()) let routes = self.state.routes let new_page:C = undefined routes.forEach(r => { let p = r.url.in(slug).map(r.page) if (p.kind == "some") { new_page = p.value return false } return true }) window.history.pushState(`${self.props.base_url}${slug}`, `${self.props.base_url}${slug}`, `${self.props.base_url}${slug}`) let new_context:Context = {...self.state.context, current_page:new_page, logic_frame:self.state.context.logic_frame + 1} let new_state:ApplicationState = {...self.state, context:new_context} self.setState(new_state) } window.onpopstate = function(e) { load() } } history = Immutable.Stack() context_from_props(props:ApplicationProps, p:C) : Context { let self = this return { current_page:p, logic_frame:0, force_reload:(callback) => make_C(ctxt => inner_callback => { if (this.state.kind == "loading routes") return null let old_context = this.state.context let new_state:ApplicationState = {...this.state, context:{...old_context, logic_frame:this.state.context.logic_frame+1}} this.setState(new_state, () => inner_callback(callback)(null)) return null }), set_page:function(x:T, new_page:Route, callback?:()=>void) { let out = new_page.url.out(x) window.history.pushState(`${self.props.base_url}${out}`, `${self.props.base_url}${out}`, `${self.props.base_url}${out}`) if (self.history.isEmpty() || self.history.peek() != out) self.history = self.history.push(out) // console.log("set page", self.history.toArray()) return make_C(ctxt => inner_callback => { if (self.state.kind == "loading routes") return undefined let new_context:Context = {...self.state.context, current_page:new_page.page(x)} let new_state:ApplicationState = {...this.state, context:new_context} self.setState(new_state, () => inner_callback(callback)(null)) return null }) }, set_url:function(x:T, new_url:Url, callback?:()=>void) { let out = new_url.out(x) // console.log("set page", self.props.base_url, out, new_url) window.history.pushState(`${self.props.base_url}${out}`, `${self.props.base_url}${out}`, `${self.props.base_url}${out}`) if (self.history.isEmpty() || self.history.peek() != out) self.history = self.history.push(out) // console.log("set url", self.history.toArray()) return unit(null) }, push_route:(new_route, callback) => make_C(ctxt => inner_callback => { if (this.state.kind == "loading routes") return null let old_context = this.state.context let new_state:ApplicationState = {...this.state, routes:this.state.routes.push(new_route)} this.setState(new_state, () => inner_callback(callback)(null)) return null }), set_routes:(routes, callback) => make_C(ctxt => inner_callback => { if (this.state.kind == "loading routes") return null let old_context = this.state.context let new_state:ApplicationState = {...this.state, routes:Immutable.List>(routes)} this.setState(new_state, () => inner_callback(callback)(null)) return null }), } } render() { if (this.state.kind == "loading routes") return
Loading...
return
{ this.state.context.current_page.comp(() => this.state.kind != "loading routes" && this.state.context)(callback => _ => callback && callback()) }
} } export let application = (mode:Mode, base_url:string, slug:string, routes:() => Promise>>) : JSX.Element => { console.log("Calling application with", window.location.href, slug, base_url) return React.createElement(Application, { mode:mode, base_url:base_url, slug:slug, routes:routes }) } export let get_context = function(key?:string, dbg?:() => string) : C { return make_C(ctxt => cont => (unit(ctxt()).comp(ctxt)(cont))) } export let link_to_route = function(label:string, x:T, r:Route, key?:string, className?:string) : C { return button(label)(null).then(key, _ => get_context().then(undefined, c => c.set_page(x, r), className).ignore()) }