sourceafDuvet::HtmlInjector.fan

using afIoc
using util

** (Service) - Injects HTML elements into your page.
** Elements are queued up and injected just before the page is sent to the browser.
** 
** Elements are listed in the HTML in the order they are added.
** Duplicate elements are ignored. 
** So if a component adds a stylesheet link, that component may be used many times on a page but, only ONE link to that stylesheet will be rendered.
const mixin HtmlInjector {
    
    ** Injects a '<meta>' element into the bottom of your head. Example:
    ** 
    **   injectMeta.withName("viewport").withContent("initial-scale=1")
    ** 
    ** will render the following tag:
    ** 
    **   <meta name="viewport" content="initial-scale=1">
    abstract MetaTagBuilder injectMeta()

    ** Injects a '<link>' element into the bottom of your head. Example:
    ** 
    **   injectLink.fromClientUrl(`/css/styles.css`)
    ** 
    ** will render the following tag:
    ** 
    **   <link href="/css/styles.css">
    abstract LinkTagBuilder injectLink()
    
    ** Injects a '<link>' element, defaulted for CSS stylesheets, into the bottom of your head. Example:
    ** 
    **   injectStylesheet.fromClientUrl(`/css/styles.css`)
    ** 
    ** will render the following tag:
    ** 
    **   <link type="text/css" rel="stylesheet" href="/css/styles.css">
    abstract LinkTagBuilder injectStylesheet()

    ** Injects a '<script>' element into the bottom of your body. Example:
    ** 
    **   injectScript.fromExternalUrl(`//code.jquery.com/jquery-2.1.1.min.js`)
    ** 
    ** will render the following tag:
    ** 
    **   <script type="text/javascript" src="//code.jquery.com/jquery-2.1.1.min.js"></script>
    ** 
    ** *Consider using [RequireJS]`http://requirejs.org/` AMD modules instead!*
    abstract ScriptTagBuilder injectScript(Bool appendToHead := false)

    ** Wraps the 'script' in a function call to [RequireJS]`http://requirejs.org/`, ensuring the given module dependencies are available.  
    ** 
    ** 'functionParams' is a map of 'RequireJs' module names to function parameter names.
    ** Example:
    ** 
    **   injectRequireScript(["jquery":"\$"], "\$('p').addClass('magic');")
    ** 
    ** will generate:
    ** 
    **   <script type="text/javascript">
    **     require(["jquery"], function ($) {
    **        $('p').addClass('magic');
    **     });
    **   </script>
    abstract ScriptTagBuilder injectRequireScript(Str:Str functionParams, Str script)

    ** Injects a call to a [RequireJS]`http://requirejs.org/` module. 
    ** 
    ** If the [RequireJS module exposes an object]`http://requirejs.org/docs/api.html#defdep` then a function may be invoked using 'funcName' and 'funcArgs'. 
    ** Example:
    ** 
    **   injectRequireCall("my/shirt", "addToCart", ["shirt", 1.99f])
    ** 
    ** will generate:
    ** 
    **   <script type="text/javascript">
    **     require(["my/shirt"], function (module) { module.addToCart("shirt", 1.99); });
    **   </script>
    ** 
    ** Or, if the [RequireJS module returns function as its module definition]`http://requirejs.org/docs/api.html#funcmodule` then it may be invoked directly by passing 'null' as the 'funcName'.
    ** Example:
    ** 
    **   injectRequireCall("my/title", null, ["Reduced to Clear!"])
    ** 
    ** will generate:
    ** 
    **   <script type="text/javascript">
    **     require(["my/title"], function (module) { module("Reduced to Clear!"); });
    **   </script>
    ** 
    ** Note that 'funcArgs' are converted into JSON; which is really useful, as it means *you* don't have to!
    abstract ScriptTagBuilder injectRequireCall(Str moduleId, Str? funcName := null, Obj?[]? funcArgs := null)
    
    ** Appends the given 'HtmlNode' to the bottom of the head section.
    ** Returns 'this'.
    @NoDoc
    abstract HtmlInjector appendToHead(HtmlNode node)
    
    ** Appends the given 'HtmlNode' to the bottom of the body section.
    ** Returns 'this'.
    @NoDoc
    abstract HtmlInjector appendToBody(HtmlNode node)
}

internal const class HtmlInjectorImpl : HtmlInjector {
    @Inject private const Registry          registry
    @Inject private const DuvetProcessor    duvetProcessor
    
    new make(|This|in) { in(this) }
    
    override MetaTagBuilder injectMeta() {
        bob := MetaTagBuilder()
        appendToHead(bob.htmlNode)
        return bob
    }

    override LinkTagBuilder injectLink() {
        bob := (LinkTagBuilder) registry.autobuild(LinkTagBuilder#)
        appendToHead(bob.htmlNode)
        return bob
    }

    override LinkTagBuilder injectStylesheet() {
        injectLink.withRel("stylesheet").withType(MimeType("text/css"))
    }

    override ScriptTagBuilder injectScript(Bool inHead := false) {
        bob := (ScriptTagBuilder) registry.autobuild(ScriptTagBuilder#)
        if (inHead)
            appendToHead(bob.htmlNode)
        else
            appendToBody(bob.htmlNode)
        return bob
    }
    
    override ScriptTagBuilder injectRequireScript(Str:Str scriptParams, Str script) {
        duvetProcessor.addRequireJs
        params  := scriptParams.keys.join(", ") { "\"${it}\"" }
        args    := scriptParams.vals.join(", ")
        script  =  script.trim.isEmpty ? " " : "\n" + script + "\n"
        body    := """require([${params}], function (${args}) {${script}});"""
        return injectScript.withScript(body)
    }
    
    override ScriptTagBuilder injectRequireCall(Str moduleId, Str? funcName := null, Obj?[]? funcArgs := null) {
        fCall := Str.defVal
        if (funcName != null || funcArgs != null) {
            fName := (funcName == null) ? Str.defVal : "." + funcName
            fArgs := (funcArgs == null) ? Str.defVal : funcArgs.join(", ") { JsonOutStream.writeJsonToStr(it) }
            fCall  = "module${fName}(${fArgs});"
        }
        return injectRequireScript([moduleId:"module"], fCall)
    }
    
    override HtmlInjector appendToHead(HtmlNode node) {
        duvetProcessor.appendToHead(node)
        return this
    }
    
    override HtmlInjector appendToBody(HtmlNode node) {
        duvetProcessor.appendToBody(node)
        return this
    }   
}