git.s-ol.nu mmm / abefbf8 mmm / mmmfs / browser.moon
abefbf8

Tree @abefbf8 (Download .tar.gz)

browser.moon @abefbf8raw · history · blame

require = relative ..., 1
import Key from require '.fileder'
import converts from require '.plugins'
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 languages from require 'mmm.highlighting'

keep = (var) ->
  last = var\get!
  var\map (val) ->
    last = val or last
    last

casts = {
  {
    inp: 'text/.*',
    out: 'mmm/dom',
    cost: 0
    transform: (val) =>
      lang = @from\match 'text/(.*)'
      languages[lang] val
  }
  {
    inp: 'URL.*'
    out: 'mmm/dom'
    cost: 0
    transform: (href) => span a (code href), :href
  }
}

for convert in *converts
  table.insert casts, convert

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
    @active = @path\map @root\walk

    -- currently active facet
    -- (re)set to default when @active changes
    @facet = ReactiveVar Key facet, 'mmm/dom'
    if MODE == 'CLIENT'
      @active\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, 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 @active\map (fileder) ->
          onchange = (_, e) ->
            { :type } = @facet\get!
            @facet\set Key name: e.target.value, :type

          current = @facet\get!
          current = current and current.name
          with 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 (-> content\set @get_content 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'

  -- 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 = @active\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 = @active\get!
      key = active and active\find prop
      key = key.original if key and key.original
      key

    @inspect_err = ReactiveVar!

    with div class: 'view inspector'
      \append nav {
        span 'inspector'
        @inspect_prop\map (current) ->
          current = current and current\tostring!
          fileder = @active\get!

          onchange = (_, e) ->
            return if e.target.value == ''
            { :name } = @facet\get!
            @inspect_prop\set Key e.target.value

          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
        @inspect\map (enabled) ->
          if enabled
            button 'close', onclick: (_, e) -> @inspect\set false
      }
      \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, (prop) =>
            value, key = @get prop
            assert key, "couldn't @get #{prop}"

            conversions = get_conversions 'mmm/dom', key.type, casts
            assert conversions, "cannot cast '#{key.type}'"
            apply_conversions conversions, value, @, prop

  default_convert = (key) => @get key.name, 'mmm/dom'

  navigate: (new) =>
    @path\set new

  todom: => tohtml @

{
  :Browser
}