diff --git a/functions/string.go b/functions/string.go
index 1603b02..4f68ac9 100644
--- a/functions/string.go
+++ b/functions/string.go
@@ -1,8 +1,17 @@
package functions
+import "html/template"
+
func FirstLetter(s string) string {
if len(s) == 0 {
return ""
}
return string(s[:1])
}
+
+func Safe(s string) template.HTML {
+ if len(s) == 0 {
+ return ""
+ }
+ return template.HTML(s)
+}
diff --git a/templating/engine.go b/templating/engine.go
index d20fe6d..cd9f5d2 100644
--- a/templating/engine.go
+++ b/templating/engine.go
@@ -48,6 +48,7 @@ func (e *Engine) Funcs(app *app.KGPZ) error {
e.AddFunc("FirstLetter", functions.FirstLetter)
e.AddFunc("Upper", strings.ToUpper)
e.AddFunc("Lower", strings.ToLower)
+ e.AddFunc("Safe", functions.Safe)
// App specific
e.AddFunc("GetAgent", app.Library.Agents.Item)
diff --git a/views/assets/js/class-tools.js b/views/assets/js/class-tools.js
new file mode 100644
index 0000000..f9520da
--- /dev/null
+++ b/views/assets/js/class-tools.js
@@ -0,0 +1,97 @@
+(function() {
+ function splitOnWhitespace(trigger) {
+ return trigger.split(/\s+/)
+ }
+
+ function parseClassOperation(trimmedValue) {
+ var split = splitOnWhitespace(trimmedValue)
+ if (split.length > 1) {
+ var operation = split[0]
+ var classDef = split[1].trim()
+ var cssClass
+ var delay
+ if (classDef.indexOf(':') > 0) {
+ var splitCssClass = classDef.split(':')
+ cssClass = splitCssClass[0]
+ delay = htmx.parseInterval(splitCssClass[1])
+ } else {
+ cssClass = classDef
+ delay = 100
+ }
+ return {
+ operation,
+ cssClass,
+ delay
+ }
+ } else {
+ return null
+ }
+ }
+
+ function performOperation(elt, classOperation, classList, currentRunTime) {
+ setTimeout(function() {
+ elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass)
+ }, currentRunTime)
+ }
+
+ function toggleOperation(elt, classOperation, classList, currentRunTime) {
+ setTimeout(function() {
+ setInterval(function() {
+ elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass)
+ }, classOperation.delay)
+ }, currentRunTime)
+ }
+
+ function processClassList(elt, classList) {
+ var runs = classList.split('&')
+ for (var i = 0; i < runs.length; i++) {
+ var run = runs[i]
+ var currentRunTime = 0
+ var classOperations = run.split(',')
+ for (var j = 0; j < classOperations.length; j++) {
+ var value = classOperations[j]
+ var trimmedValue = value.trim()
+ var classOperation = parseClassOperation(trimmedValue)
+ if (classOperation) {
+ if (classOperation.operation === 'toggle') {
+ toggleOperation(elt, classOperation, classList, currentRunTime)
+ currentRunTime = currentRunTime + classOperation.delay
+ } else {
+ currentRunTime = currentRunTime + classOperation.delay
+ performOperation(elt, classOperation, classList, currentRunTime)
+ }
+ }
+ }
+ }
+ }
+
+ function maybeProcessClasses(elt) {
+ if (elt.getAttribute) {
+ var classList = elt.getAttribute('classes') || elt.getAttribute('data-classes')
+ if (classList) {
+ processClassList(elt, classList)
+ }
+ }
+ }
+
+ htmx.defineExtension('class-tools', {
+ onEvent: function(name, evt) {
+ if (name === 'htmx:afterProcessNode') {
+ var elt = evt.detail.elt
+ maybeProcessClasses(elt)
+ var classList = elt.getAttribute("apply-parent-classes") || elt.getAttribute("data-apply-parent-classes");
+ if (classList) {
+ var parent = elt.parentElement;
+ parent.removeChild(elt);
+ parent.setAttribute("classes", classList);
+ maybeProcessClasses(parent);
+ } else if (elt.querySelectorAll) {
+ var children = elt.querySelectorAll('[classes], [data-classes]')
+ for (var i = 0; i < children.length; i++) {
+ maybeProcessClasses(children[i])
+ }
+ }
+ }
+ }
+ })
+})()
diff --git a/views/assets/js/client-side-templates.js b/views/assets/js/client-side-templates.js
new file mode 100644
index 0000000..cf414ff
--- /dev/null
+++ b/views/assets/js/client-side-templates.js
@@ -0,0 +1,96 @@
+htmx.defineExtension('client-side-templates', {
+ transformResponse: function(text, xhr, elt) {
+ var mustacheTemplate = htmx.closest(elt, '[mustache-template]')
+ if (mustacheTemplate) {
+ var data = JSON.parse(text)
+ var templateId = mustacheTemplate.getAttribute('mustache-template')
+ var template = htmx.find('#' + templateId)
+ if (template) {
+ return Mustache.render(template.innerHTML, data)
+ } else {
+ throw new Error('Unknown mustache template: ' + templateId)
+ }
+ }
+
+ var mustacheArrayTemplate = htmx.closest(elt, '[mustache-array-template]')
+ if (mustacheArrayTemplate) {
+ var data = JSON.parse(text)
+ var templateId = mustacheArrayTemplate.getAttribute('mustache-array-template')
+ var template = htmx.find('#' + templateId)
+ if (template) {
+ return Mustache.render(template.innerHTML, { data })
+ } else {
+ throw new Error('Unknown mustache template: ' + templateId)
+ }
+ }
+
+ var handlebarsTemplate = htmx.closest(elt, '[handlebars-template]')
+ if (handlebarsTemplate) {
+ var data = JSON.parse(text)
+ var templateId = handlebarsTemplate.getAttribute('handlebars-template')
+ var templateElement = htmx.find('#' + templateId).innerHTML
+ var renderTemplate = Handlebars.compile(templateElement)
+ if (renderTemplate) {
+ return renderTemplate(data)
+ } else {
+ throw new Error('Unknown handlebars template: ' + templateId)
+ }
+ }
+
+ var handlebarsArrayTemplate = htmx.closest(elt, '[handlebars-array-template]')
+ if (handlebarsArrayTemplate) {
+ var data = JSON.parse(text)
+ var templateId = handlebarsArrayTemplate.getAttribute('handlebars-array-template')
+ var templateElement = htmx.find('#' + templateId).innerHTML
+ var renderTemplate = Handlebars.compile(templateElement)
+ if (renderTemplate) {
+ return renderTemplate(data)
+ } else {
+ throw new Error('Unknown handlebars template: ' + templateId)
+ }
+ }
+
+ var nunjucksTemplate = htmx.closest(elt, '[nunjucks-template]')
+ if (nunjucksTemplate) {
+ var data = JSON.parse(text)
+ var templateName = nunjucksTemplate.getAttribute('nunjucks-template')
+ var template = htmx.find('#' + templateName)
+ if (template) {
+ return nunjucks.renderString(template.innerHTML, data)
+ } else {
+ return nunjucks.render(templateName, data)
+ }
+ }
+
+ var xsltTemplate = htmx.closest(elt, '[xslt-template]')
+ if (xsltTemplate) {
+ var templateId = xsltTemplate.getAttribute('xslt-template')
+ var template = htmx.find('#' + templateId)
+ if (template) {
+ var content = template.innerHTML
+ ? new DOMParser().parseFromString(template.innerHTML, 'application/xml')
+ : template.contentDocument
+ var processor = new XSLTProcessor()
+ processor.importStylesheet(content)
+ var data = new DOMParser().parseFromString(text, 'application/xml')
+ var frag = processor.transformToFragment(data, document)
+ return new XMLSerializer().serializeToString(frag)
+ } else {
+ throw new Error('Unknown XSLT template: ' + templateId)
+ }
+ }
+
+ var nunjucksArrayTemplate = htmx.closest(elt, '[nunjucks-array-template]')
+ if (nunjucksArrayTemplate) {
+ var data = JSON.parse(text)
+ var templateName = nunjucksArrayTemplate.getAttribute('nunjucks-array-template')
+ var template = htmx.find('#' + templateName)
+ if (template) {
+ return nunjucks.renderString(template.innerHTML, { data })
+ } else {
+ return nunjucks.render(templateName, { data })
+ }
+ }
+ return text
+ }
+})
diff --git a/views/assets/js/head-support.js b/views/assets/js/head-support.js
new file mode 100644
index 0000000..b1e6878
--- /dev/null
+++ b/views/assets/js/head-support.js
@@ -0,0 +1,144 @@
+//==========================================================
+// head-support.js
+//
+// An extension to add head tag merging.
+//==========================================================
+(function(){
+
+ var api = null;
+
+ function log() {
+ //console.log(arguments);
+ }
+
+ function mergeHead(newContent, defaultMergeStrategy) {
+
+ if (newContent && newContent.indexOf('
-1) {
+ const htmlDoc = document.createElement("html");
+ // remove svgs to avoid conflicts
+ var contentWithSvgsRemoved = newContent.replace(/