require = relative ..., 1
import Key from require '.fileder'
import get_conversions, apply_conversions from require '.conversion'
import ReactiveVar, get_or_create, text, elements, tohtml from require 'mmm.component'
import pre, div, nav, span, button, a, code, select, option from elements
import link_to from (require '.util') elements
import languages from require 'mmm.highlighting'
keep = (var) ->
last = var\get!
var\map (val) ->
last = val or last
last
combine = (...) ->
res = {}
lists = {...}
for list in *lists
for val in *list
table.insert res, val
res
casts = {
{
inp: 'URL.*'
out: 'mmm/dom'
cost: 0
transform: (href) => span a (code href), :href
}
}
get_casts = -> combine casts, PLUGINS.converts, PLUGINS.editors
export BROWSER
class Browser
new: (@root, path, facet, rehydrate=false) =>
BROWSER = @
-- root fileder
assert @root, 'root fileder is nil'
-- active path
@path = ReactiveVar path or ''
-- active fileder
-- (re)set every time @path changes
@fileder = @path\map @root\walk
-- currently active facet
-- (re)set to default when @fileder changes
@facet = ReactiveVar Key facet, 'mmm/dom'
if MODE == 'CLIENT'
@fileder\subscribe (fileder) ->
return unless fileder
last = @facet and @facet\get!
@facet\set Key if last then last.type else 'mmm/dom'
-- update URL bar
if MODE == 'CLIENT'
logo = document\querySelector 'header h1 > a > svg'
spin = ->
logo.classList\add 'spin'
logo.parentElement.offsetWidth
logo.classList\remove 'spin'
@facet\subscribe (facet) ->
document.body.classList\add 'loading'
spin!
return if @skip
path = @path\get!
state = js.global\eval 'new Object()'
state.path = path
state.name = facet.name
state.type = facet.type
window.history\pushState state, '', "#{path}/#{(Key facet.name, 'text/html+interactive')\tostring true}"
window.onpopstate = (_, event) ->
state = event.state
if state != js.null
@skip = true
@path\set state.path
@facet\set Key state.name, state.type
@skip = nil
-- whether inspect tab is active
@inspect = ReactiveVar (MODE == 'CLIENT' and window.location.hash == '#inspect')
-- retrieve or create the wrapper and main elements
main = get_or_create 'div', 'browser-root', class: 'main view'
-- prepend the navbar
if MODE == 'SERVER'
main\append nav { id: 'browser-navbar', span 'please stand by... interactivity loading :)' }
else
main\prepend with get_or_create 'nav', 'browser-navbar'
.node.innerHTML = ''
\append span 'path: ', @path\map (path) -> with div class: 'path', style: { display: 'inline-block' }
path_segment = (name, href) ->
a name, href: "#{href}/", onclick: (_, e) ->
e\preventDefault!
@navigate href
href = ''
path = path\match '^/(.*)'
\append path_segment 'root', ''
while path
name, rest = path\match '^([%w%-_%.]+)/(.*)'
if not name
name = path
path = rest
href = "#{href}/#{name}"
\append '/'
\append path_segment name, href
\append span 'view facet:', style: { 'margin-right': '0' }
\append @fileder\map (fileder) ->
onchange = (_, e) ->
{ :type } = @facet\get!
@facet\set Key name: e.target.value, :type
current = @facet\get!
current = current and current.name
with elements.select :onchange, disabled: not fileder, value: @facet\map (f) -> f and f.name
has_main = fileder and fileder\has_facet ''
\append option '(main)', value: '', disabled: not has_main, selected: current == ''
if fileder
for i, value in ipairs fileder\get_facets!
continue if value == ''
\append option value, :value, selected: value == current
\append @inspect\map (enabled) ->
if not enabled
button 'inspect', onclick: (_, e) -> @inspect\set true
@error = ReactiveVar!
main\append with get_or_create 'div', 'browser-error', class: @error\map (e) -> if e then 'error-wrap' else 'error-wrap empty'
\append (span "an error occured while rendering this view:"), (rehydrate and .node.firstChild)
\append @error
-- append or patch #browser-content
main\append with get_or_create 'div', 'browser-content', class: 'content'
@content = ReactiveVar if rehydrate then .node.lastChild else @get_content @facet\get!
\append keep @content
if MODE == 'CLIENT'
@facet\subscribe (p) ->
window\setTimeout (-> @refresh p), 150
if rehydrate
-- force one rerender to set onclick handlers etc
@facet\set @facet\get!
inspector = @inspect\map (enabled) -> if enabled then @get_inspector!
-- export mmm/component interface
wrapper = get_or_create 'div', 'browser-wrapper', main, inspector, class: 'browser'
@node = wrapper.node
@render = wrapper\render
err_and_trace = (msg) -> debug.traceback msg, 2
default_convert = (key) => @get key.name, 'mmm/dom'
-- rerender main content
refresh: (facet=@facet\get!) =>
if facet == true -- deep refresh
@fileder\transform (i) -> i
else
@content\set @get_content facet
-- render #browser-content
get_content: (prop, err=@error, convert=default_convert) =>
clear_error = ->
err\set! if MODE == 'CLIENT'
disp_error = (msg) ->
if MODE == 'CLIENT'
err\set pre msg
warn "ERROR rendering content: #{msg}"
div!
active = @fileder\get!
return disp_error "fileder not found!" unless active
return disp_error "facet not found!" unless prop
ok, res = xpcall convert, err_and_trace, active, prop
document.body.classList\remove 'loading' if MODE == 'CLIENT'
if ok and res
clear_error!
res
elseif ok
div "[no conversion path to #{prop.type}]"
elseif res and res\match '%[nossr%]'
div "[this page could not be pre-rendered on the server]"
else
disp_error res
get_inspector: =>
-- active facet in inspect tab
-- (re)set to match when @facet changes
@inspect_prop = @facet\map (prop) ->
active = @fileder\get!
key = active and active\find prop
key = key.original if key and key.original
key
@inspect_err = ReactiveVar!
@editor = ReactiveVar!
with div class: 'view inspector'
-- nav
\append nav {
span 'inspector'
button 'close', onclick: (_, e) -> @inspect\set false
}
\append div {
class: 'subnav'
ondrop: ->
print "dropped"
onpaste: ->
print "pasted"
@inspect_prop\map (current) ->
current = current and current\tostring!
fileder = @fileder\get!
onchange = (_, e) ->
facet = e.target.value
return if facet == ''
@inspect_prop\set Key facet
with select :onchange
\append option '(none)', value: '', disabled: true, selected: not value
if fileder
for value in pairs fileder.facet_keys
\append option value, :value, selected: value == current
button 'rm', class: 'tight', onclick: (_, e) ->
if window\confirm "continuing will permanently remove the facet '#{@inspect_prop\get!}'."
fileder = @fileder\get!
fileder\set @inspect_prop\get!, nil
@refresh true
button 'add', class: 'tight', onclick: (_, e) ->
facet = window\prompt "please enter the facet string ('name: type' or 'type'):", 'text/markdown'
return if not facet or facet == '' or facet == js.null
fileder = @fileder\get!
fileder\set facet, ''
@inspect_prop\set Key facet
@refresh!
div style: flex: '1'
@editor\map (e) -> e and e.saveBtn
}
-- error / content
\append with div class: @inspect_err\map (e) -> if e then 'error-wrap' else 'error-wrap empty'
\append span "an error occured while rendering this view:"
\append @inspect_err
\append with pre class: 'content'
\append keep @inspect_prop\map (prop, old) ->
@get_content prop, @inspect_err, (fileder, prop) ->
value, key = fileder\get prop
assert key, "couldn't @get #{prop}"
conversions = get_conversions 'mmm/dom', key.type, get_casts!
assert conversions, "cannot cast '#{key.type}'"
with res = apply_conversions conversions, value, fileder, prop
@editor\set if res.EDITOR then res
-- children
\append nav {
class: 'thing'
span 'children'
button 'add', onclick: (_, e) ->
name = window\prompt "please enter the name of the child fileder:", 'unnamed_fileder'
return if not name or name == '' or name == js.null
child = @fileder\get!\add_child name
@refresh true
}
\append @fileder\map (fileder) ->
with div class: 'children'
num = #fileder.children
for i, child in ipairs fileder.children
name = child\gett 'name: alpha'
\append div {
style:
display: 'flex'
'justify-content': 'space-between'
span '- ', (link_to child, code name), style: flex: 1
button '↑', disabled: i == 1, onclick: (_, e) ->
fileder\swap_children i, i - 1
@refresh true
button '↓', disabled: i == num, onclick: (_, e) ->
fileder\swap_children i, i + 1
@refresh true
button 'rm', onclick: (_, e) ->
if window\confirm "continuing will permanently remove all content in '#{child.path}'."
fileder\remove_child i
@refresh true
}
default_convert = (key) => @get key.name, 'mmm/dom'
navigate: (new) =>
@path\set new
todom: => tohtml @
{
:Browser
}