using afIoc
using afBedSheet
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.
**
** Injecting scripts and stylesheets will also update an 'Content-Security-Policy' directives to ensure the added content can be executed.
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!*
**
** Note that by default the script is injected at the bottom of the '<body>' tag.
abstract ScriptTagBuilder injectScript(Bool appendToHead := false)
** Ensures that the RequireJS script and corresponding config is injected into the page.
**
** A call to this is only required when you want to hard code require calls in the HTML.
** For example, if your HTML looked like this:
**
** pre>
** <html>
** <body>
** <h1>Hello!</h1>
** <script>
** require(['jquery'], function($) {
** // ... wotever...
** });
** </script>
** </body>
** </html>
** <pre
**
** Then a call to 'injectRequireJs()' would be required to ensure RequireJS was loaded before the script call.
abstract Void injectRequireJs()
** 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:
**
** injectRequireModule("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 a 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 injectRequireModule(Str moduleId, Str? funcName := null, Obj?[]? funcArgs := null)
** Injects a call to a Fantom method. That's right, this method lets you run Fantom code in your web browser!
** Because Fantom only compiles classes with the '@Js' facet into Javascript, ensure the method's class has it!
**
** All method arguments must be '@Serializable' as they are serialised into Strings and embedded directly into Javascript.
**
** 'env' are environment variables passed into the Fantom Javascript runtime.
**
** Note that when instantiating an FWT window, by default it takes up the whole browser window.
** To constrain the FWT window to a particular element on the page, pass in the follow environment variable:
**
** "fwt.window.root" : "<element-id>"
**
** Where '<element-id>' is the html ID of an element on the page. The FWT window will attach itself to this element.
**
** syntax: fantom
** htmlInjector.injectFantomMethod(FwtExample#info, null, ["fwt.window.root" : "<element-id>"])
**
** Note that the element needs to specify a width, height and give a CSS position of 'relative'.
** This may either be done in CSS or defined on the element directly:
**
** <div id="fwt-window" style="width: 640px; height:480px; position:relative;"></div>
abstract ScriptTagBuilder injectFantomMethod(Method method, Obj?[]? args := null, [Str:Str]? env := null)
** Appends the given HTML Str (or 'afDuvet::HtmlNode') to the bottom of the head section.
** Returns 'this'.
abstract HtmlInjector appendToHead(Obj html)
** Appends the given HTML Str (or 'afDuvet::HtmlNode') to the bottom of the body section.
** Returns 'this'.
abstract HtmlInjector appendToBody(Obj html)
}
internal const class HtmlInjectorImpl : HtmlInjector {
@Inject private const Scope scope
@Inject private const DuvetProcessor duvetProcessor
@Inject private const PodHandler podHandler
new make(|This|in) { in(this) }
override MetaTagBuilder injectMeta() {
bob := MetaTagBuilder()
appendToHead(bob.htmlNode)
return bob
}
override LinkTagBuilder injectLink() {
bob := (LinkTagBuilder) scope.build(LinkTagBuilder#)
appendToHead(bob.htmlNode)
return bob
}
override LinkTagBuilder injectStylesheet() {
injectLink.withRel("stylesheet").withType(MimeType("text/css"))
}
override ScriptTagBuilder injectScript(Bool inHead := false) {
bob := (ScriptTagBuilder) scope.build(ScriptTagBuilder#)
if (inHead)
appendToHead(bob.htmlNode)
else
appendToBody(bob.htmlNode)
return bob
}
override Void injectRequireJs() {
duvetProcessor.addRequireJs
}
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 injectRequireModule(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 ScriptTagBuilder injectFantomMethod(Method method, Obj?[]? args := null, [Str:Str]? env := null) {
if (!method.parent.hasFacet(Js#))
throw ArgErr(ErrMsgs.htmlInjector_noJsFacet(method.parent))
podName := method.parent.pod.name
jsParam := [podName:"_${podName}"]
argStrs := args == null ? Str#.emptyList : args.map { Buf().writeObj(it).flip.readAllStr }
jargs := argStrs.map |Str arg->Str| { "args.add(fan.sys.Str.toBuf(${arg.toCode}).readObj());" }
envs := env?.rw ?: Str:Str[:]
if (!envs.containsKey("sys.uriPodBase") && podHandler.baseUrl != null)
envs["sys.uriPodBase"] = podHandler.baseUrl.toStr
envStr := StrBuf()
if (envs?.size > 0) {
envStr.add("var env = fan.sys.Map.make(fan.sys.Str.\$type, fan.sys.Str.\$type);\n")
envStr.add("env.caseInsensitive\$(true);\n")
envs.each |v, k| {
v = v.toCode('\'')
if (k == "sys.uriPodBase")
envStr.add("fan.sys.UriPodBase = $v;\n")
else
envStr.add("env.set('$k', $v);\n")
}
envStr.add("fan.sys.Env.cur().\$setVars(env);\n")
}
// Fantom TimeZone - see http://fantom.org/forum/topic/2548
// tz.js is annoying because it has to come *after* sys but before the app
// the easiest way to do this is to wrap the injected code in a nested require()
script :=
"
// default the tz to a sensible default that doesn't cause errors
if (fan.sys.TimeZone.m_cur == null)
fan.sys.TimeZone.m_cur = fan.sys.TimeZone.fromStr('UTC');
// actually, lets just load the tz database
require(['sysTz'], function(foo) {
// inject env vars
$envStr.toStr
// construct method args
var args = fan.sys.List.make(fan.sys.Obj.\$type);
${jargs.join('\n'.toChar)}
// find main
var qname = '$method.qname';
var main = fan.sys.Slot.findMethod(qname);
// invoke main
if (main.isStatic()) main.callList(args);
else main.callOn(main.parent().make(), args);
});
"
return injectRequireScript(jsParam, script)
}
override HtmlInjector appendToHead(Obj html) {
node := html as HtmlNode
if (node == null && html is Str)
node = HtmlText(html)
if (node == null)
throw Err("html arg must be Str or ${HtmlNode#.qname}: ${html.typeof} - $html")
duvetProcessor.appendToHead(node)
return this
}
override HtmlInjector appendToBody(Obj html) {
node := html as HtmlNode
if (node == null && html is Str)
node = HtmlText(html)
if (node == null)
throw Err("html arg must be Str or ${HtmlNode#.qname}: ${html.typeof} - $html")
duvetProcessor.appendToBody(node)
return this
}
}