sourceafFancordionBootstrap::BootstrapSkin.fan

using afFancordion

** A Fancordion Skin for Twitter [Bootstrap]`http://getbootstrap.com/`.
** 
** To use the vanilla 'Bootstrap' skin, set the 'skinType' field on [FancordionRunner]`afFancordion::FancordionRunner`:
** 
**   syntax: fantom
** 
**   using afFancordion
**   using afFancordionBootstrap
** 
**   ** My Bootstrap Fixture
**   class BootstrapFixture : FixtureTest {
**       override FancordionRunner fancordionRunner() {
**           FancordionRunner() {
**               it.skinType = BootstrapSkin#
**           }
**       }
** 
**       ...
**   }
** 
** To use the themed version of Bootstrap, set the 'gimmeSomeSkin' field instead:
** 
**   syntax: fantom
**   it.gimmeSomeSkin = |->FancordionSkin| { BootstrapSkin(true) }
** 
class BootstrapSkin : FancordionSkin {
    private Bool    inPanelHeading
    private Int     nextErrId   := 1
    private Str[]   modals      := Str[,]
    private Bool    useTheme
    
    ** The CSS classes used for tables. Set to enhance your tables:
    ** 
    **   syntax: fantom
    **   skin.tableCss = "table table-striped table-bordered table-hover table-condensed"
    ** 
    ** Defaults to just 'table'.
    ** See [Bootstrap Tables]`http://getbootstrap.com/css/#tables` for details.
            Str     tableCss    := "table" 
    
    ** Creates a Bootstrap skin. 
    ** Set 'useTheme' to 'true' to use the *themed* version of Bootstrap. 
    ** The *themed* version uses colour gradients on buttons and other components.  
    new make(Bool useTheme := false) {
        this.useTheme = useTheme
    }
    
    // ---- Setup / Tear Down -------------------

    @NoDoc
    override Void tearDown() {
        super.tearDown
    }

    // ---- HTML Methods ------------------------

    @NoDoc
    override This head() {
        super.head
        write("""<meta name="viewport" content="width=device-width, initial-scale=1" />\n""")

        // copy the bootstrap files over
        addCss      (bootstrapCssUri.get)
        if (useTheme)
            addCss  (`fan://afFancordionBootstrap/doc/skins/bootstrap/css/bootstrap-theme.min.css`.get)
        addCss      (`fan://afFancordionBootstrap/doc/skins/bootstrap/css/fancordion-bs.css`.get)

        addScript   (`fan://afFancordionBootstrap/doc/skins/bootstrap/js/jquery-1.11.3.min.js`.get)
        addScript   (`fan://afFancordionBootstrap/doc/skins/bootstrap/js/bootstrap.min.js`.get)

        copyFile    (`fan://afFancordionBootstrap/doc/skins/bootstrap/fonts/glyphicons-halflings-regular.eot`.get,  `fonts/`)
        copyFile    (`fan://afFancordionBootstrap/doc/skins/bootstrap/fonts/glyphicons-halflings-regular.ttf`.get,  `fonts/`)
        copyFile    (`fan://afFancordionBootstrap/doc/skins/bootstrap/fonts/glyphicons-halflings-regular.woff`.get, `fonts/`)
        copyFile    (`fan://afFancordionBootstrap/doc/skins/bootstrap/fonts/glyphicons-halflings-regular.woff2`.get,`fonts/`)
        return this
    }

    ** Returns the location of the Bootstrap CSS file.
    ** 
    ** Defaults to '`fan://afFancordionBootstrap/res/bootstrap/css/bootstrap.min.css`' but is overridden by Bootswatch skins.
    virtual Uri bootstrapCssUri() {
        `fan://afFancordionBootstrap/doc/skins/bootstrap/css/bootstrap.min.css`
    }
    
    @NoDoc
    override This body() {
        write("""<body>\n""")
        write("""<div class="container">\n""")
        write("""<div class="row">\n""")
        write("""<div class="col-xs-12">\n""")
        breadcrumbs
        return this
    }
    
    @NoDoc
    override This breadcrumbs() {
        idx := 0
        breadcrumbPaths := breadcrumbPaths
        write("""<ol class="breadcrumb">""")
        breadcrumbPaths.each |text, href| {
            if (++idx == breadcrumbPaths.size)
                write("""<li class="active">${text}</li>""")
            else
                write("<li>").a(href, text).write("</li>")
        }
        write("""</ol>""")
        return this
    }

    @NoDoc
    override This section() {
        write("""<div class="panel panel-info">\n""")
        write("""<div class="panel-heading">\n""")
        inPanelHeading = true
        return this
    }

    @NoDoc
    override This sectionEnd() {
        write("""</div>\n""")
        write("""</div>\n""")
        return this
    }
    
    @NoDoc
    override This heading(Int level, Str title, Str? anchorId) {
        id := (anchorId == null) ? Str.defVal : " id=\"${anchorId.toXml}\""
        return inPanelHeading
            ? write("""<h${level}${id} class="panel-title">\n""")
            : write("""<h${level}${id}>""")
    }

    @NoDoc
    override This headingEnd(Int level) {
        write("""</h${level}>\n""")
        if (inPanelHeading) {           
            write("""</div>\n""")
            write("""<div class="panel-body">\n""")
            inPanelHeading = false
        }
        return this
    }
    
    @NoDoc
    override This table(Str? cssClass := null) {
        inTable = true
        return write("""<table class="${tableCss} ${cssClass}">\n""")
    }
    
    @NoDoc
    override This footer() {
        ver := Pod.find("afFancordion").version
        now := DateTime.now(1sec).toLocale("D MMM YYYY, k:mmaa zzzz 'Time'")
        dur := DateTime.now(null) - fixtureMeta.StartTime
        write("""<footer class="clearfix">\n""")
        write("""<hr/>\n""")
        write("""<div class="pull-right small text-right">\n""")
        write("""\tResults generated by <b><a href="http://www.fantomfactory.org/pods/afFancordion">Fancordion v${ver}</a></b>\n""")
        write("""\t<div class="testTime">in ${dur.toLocale} on ${now}</div>\n""")
        write("""</div>\n""")
        write("""</footer>\n""")
        return this
    }
    
    @NoDoc
    override This bodyEnd() {
        footer
        scriptUrls.each { script(it) }
        write("""</div>\n""")
        write("""</div>\n""")
        write("""</div>\n""")
        
        modals.each { write(it) }
        
        write("""</body>\n""")
        return this
    }
    
    
    // ---- Test Results --------------------------------------------------------------------------
    
    private Str cmdHtml(Str level, Str body) {
        if (inTable)
            return """<${cmdElem} class="cmd ${level}">${body}</${cmdElem}>"""
        if (inPre)
            return """<${cmdElem} class="cmd bg-${level}">${body}</${cmdElem}>"""
        return """<${cmdElem} class="cmd alert alert-${level}">${body}</${cmdElem}>"""
    }
    
    ** Called to render an ignored command.
    @NoDoc
    override This cmdIgnored(Str text) {
        return write(cmdHtml("active", text.toXml))
    }

    ** Called to render a command success.
    @NoDoc
    override This cmdSuccess(Str text, Bool escape := true) {
        body := escape ? text.toXml : text
        return write(cmdHtml("success", body))
    }

    ** Called to render a command failure.
    @NoDoc
    override This cmdFailure(Str expected, Obj? actual, Bool escape := true) {
        text := escape ? expected.toXml : expected
        body := """<del class="expected">${text}</del> <span class="actual">${firstLine(actual?.toStr).toXml}</span>"""
        return write(cmdHtml("danger", body))
    }

    ** Called to render a command error.
    @NoDoc
    override This cmdErr(Str cmdUrl, Str cmdText, Err err) {
        modalId := "modal-${nextErrId++}"
        body := """<del class="expected">${cmdText.toXml}</del> <samp class="actual">${err.typeof.name.toXml}: ${firstLine(err.msg).toXml}</samp>"""
        write(cmdHtml("danger", body))
        write(""" <button type="button" class="btn btn-default btn-sm" data-toggle="modal" data-target="#${modalId}">Show Stack Trace</button>""")
        modal := 
"""
   <!-- Modal -->
   <div class="modal fade" id="${modalId}" tabindex="-1">
     <div class="modal-dialog modal-lg">
       <div class="modal-content">
         <div class="modal-header">
           <button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
           <h4 class="modal-title">${err.typeof.name.toXml}: ${firstLine(err.msg).toXml}</h4>
         </div>
         <div class="modal-body">
           <p>Err thrown while evaluating command: <code>${cmdUrl.toXml}</code></p>
           <pre>${err.traceToStr.toXml}</pre>
         </div>
         <div class="modal-footer">
           <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
         </div>
       </div>
     </div>
   </div>
   """
        modals.add(modal)
        return this
    }

    private Str firstLine(Str? txt) {
        txt?.splitLines?.exclude { it.trim.isEmpty }?.first ?: Str.defVal
    }
}