Introduced templating and views
							
								
								
									
										2952
									
								
								views/public/Diagram.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 117 KiB | 
							
								
								
									
										
											BIN
										
									
								
								views/public/GND.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 64 KiB | 
							
								
								
									
										71
									
								
								views/public/css/fonts.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,71 @@ | ||||
| @font-face { | ||||
| 	font-family: "Rancho"; | ||||
| 	font-style: normal; | ||||
| 	font-weight: 500; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/Rancho-Regular.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Merriweather"; | ||||
| 	font-style: normal; | ||||
| 	font-weight: 500; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/Merriweather-Regular.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Merriweather"; | ||||
| 	font-style: italic; | ||||
| 	font-weight: 500; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/Merriweather-Italic.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Merriweather"; | ||||
| 	font-style: normal; | ||||
| 	font-weight: bold; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/Merriweather-Bold.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Merriweather"; | ||||
| 	font-style: italic; | ||||
| 	font-weight: bold; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Source Sans 3"; | ||||
| 	font-style: normal; | ||||
| 	font-weight: 500; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/SourceSans3-Medium.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Source Sans 3"; | ||||
| 	font-style: italic; | ||||
| 	font-weight: 500; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/SourceSans3-MediumItalic.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Source Sans 3"; | ||||
| 	font-style: normal; | ||||
| 	font-weight: bold; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/SourceSans3-Bold.ttf) format("truetype"); | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: "Source Sans 3"; | ||||
| 	font-style: italic; | ||||
| 	font-weight: bold; | ||||
| 	font-display: swap; | ||||
| 	src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype"); | ||||
| } | ||||
							
								
								
									
										3104
									
								
								views/public/css/remixicon.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-Black.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-BlackItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-Bold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-BoldItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-Italic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-Light.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-LightItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Merriweather-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-Bold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-BoldItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-ExtraBold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-ExtraBoldItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-Italic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-Light.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-LightItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-Medium.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-MediumItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-SemiBold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/MerriweatherSans-SemiBoldItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/Rancho-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-Black.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-BlackItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-Bold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-BoldItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-ExtraBold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-ExtraBoldItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-ExtraLight.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-ExtraLightItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-Italic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-Light.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-LightItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-Medium.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-MediumItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-SemiBold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/SourceSans3-SemiBoldItalic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										3104
									
								
								views/public/fonts/remixicon.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/remixicon.eot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								views/public/fonts/remixicon.glyph.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										3106
									
								
								views/public/fonts/remixicon.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										3088
									
								
								views/public/fonts/remixicon.module.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										6145
									
								
								views/public/fonts/remixicon.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										3075
									
								
								views/public/fonts/remixicon.styl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										9196
									
								
								views/public/fonts/remixicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.6 MiB | 
							
								
								
									
										11
									
								
								views/public/fonts/remixicon.symbol.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 MiB | 
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/remixicon.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/remixicon.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								views/public/fonts/remixicon.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										5
									
								
								views/public/js/alpine.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										97
									
								
								views/public/js/class-tools.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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]) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| })() | ||||
							
								
								
									
										96
									
								
								views/public/js/client-side-templates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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 | ||||
|   } | ||||
| }) | ||||
							
								
								
									
										144
									
								
								views/public/js/head-support.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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('<head') > -1) { | ||||
|             const htmlDoc = document.createElement("html"); | ||||
|             // remove svgs to avoid conflicts | ||||
|             var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); | ||||
|             // extract head tag | ||||
|             var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im); | ||||
|  | ||||
|             // if the  head tag exists... | ||||
|             if (headTag) { | ||||
|  | ||||
|                 var added = [] | ||||
|                 var removed = [] | ||||
|                 var preserved = [] | ||||
|                 var nodesToAppend = [] | ||||
|  | ||||
|                 htmlDoc.innerHTML = headTag; | ||||
|                 var newHeadTag = htmlDoc.querySelector("head"); | ||||
|                 var currentHead = document.head; | ||||
|  | ||||
|                 if (newHeadTag == null) { | ||||
|                     return; | ||||
|                 } else { | ||||
|                     // put all new head elements into a Map, by their outerHTML | ||||
|                     var srcToNewHeadNodes = new Map(); | ||||
|                     for (const newHeadChild of newHeadTag.children) { | ||||
|                         srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|  | ||||
|  | ||||
|                 // determine merge strategy | ||||
|                 var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy; | ||||
|  | ||||
|                 // get the current head | ||||
|                 for (const currentHeadElt of currentHead.children) { | ||||
|  | ||||
|                     // If the current head element is in the map | ||||
|                     var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); | ||||
|                     var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval"; | ||||
|                     var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true"; | ||||
|                     if (inNewContent || isPreserved) { | ||||
|                         if (isReAppended) { | ||||
|                             // remove the current version and let the new version replace it and re-execute | ||||
|                             removed.push(currentHeadElt); | ||||
|                         } else { | ||||
|                             // this element already exists and should not be re-appended, so remove it from | ||||
|                             // the new content map, preserving it in the DOM | ||||
|                             srcToNewHeadNodes.delete(currentHeadElt.outerHTML); | ||||
|                             preserved.push(currentHeadElt); | ||||
|                         } | ||||
|                     } else { | ||||
|                         if (mergeStrategy === "append") { | ||||
|                             // we are appending and this existing element is not new content | ||||
|                             // so if and only if it is marked for re-append do we do anything | ||||
|                             if (isReAppended) { | ||||
|                                 removed.push(currentHeadElt); | ||||
|                                 nodesToAppend.push(currentHeadElt); | ||||
|                             } | ||||
|                         } else { | ||||
|                             // if this is a merge, we remove this content since it is not in the new head | ||||
|                             if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) { | ||||
|                                 removed.push(currentHeadElt); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Push the tremaining new head elements in the Map into the | ||||
|                 // nodes to append to the head tag | ||||
|                 nodesToAppend.push(...srcToNewHeadNodes.values()); | ||||
|                 log("to append: ", nodesToAppend); | ||||
|  | ||||
|                 for (const newNode of nodesToAppend) { | ||||
|                     log("adding: ", newNode); | ||||
|                     var newElt = document.createRange().createContextualFragment(newNode.outerHTML); | ||||
|                     log(newElt); | ||||
|                     if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) { | ||||
|                         currentHead.appendChild(newElt); | ||||
|                         added.push(newElt); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // remove all removed elements, after we have appended the new elements to avoid | ||||
|                 // additional network requests for things like style sheets | ||||
|                 for (const removedElement of removed) { | ||||
|                     if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) { | ||||
|                         currentHead.removeChild(removedElement); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed}); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     htmx.defineExtension("head-support", { | ||||
|         init: function(apiRef) { | ||||
|             // store a reference to the internal API. | ||||
|             api = apiRef; | ||||
|  | ||||
|             htmx.on('htmx:afterSwap', function(evt){ | ||||
|                 let xhr = evt.detail.xhr; | ||||
|                 if (xhr) { | ||||
|                     var serverResponse = xhr.response; | ||||
|                     if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { | ||||
|                         mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append"); | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             htmx.on('htmx:historyRestore', function(evt){ | ||||
|                 if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { | ||||
|                     if (evt.detail.cacheMiss) { | ||||
|                         mergeHead(evt.detail.serverResponse, "merge"); | ||||
|                     } else { | ||||
|                         mergeHead(evt.detail.item.head, "merge"); | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             htmx.on('htmx:historyItemCreated', function(evt){ | ||||
|                 var historyItem = evt.detail.item; | ||||
|                 historyItem.head = document.head.outerHTML; | ||||
|             }) | ||||
|         } | ||||
|     }); | ||||
|  | ||||
| })() | ||||
							
								
								
									
										130
									
								
								views/public/js/htmx-response-targets.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,130 @@ | ||||
| (function(){ | ||||
|  | ||||
|     /** @type {import("../htmx").HtmxInternalApi} */ | ||||
|     var api; | ||||
|  | ||||
|     var attrPrefix = 'hx-target-'; | ||||
|  | ||||
|     // IE11 doesn't support string.startsWith | ||||
|     function startsWith(str, prefix) { | ||||
|         return str.substring(0, prefix.length) === prefix | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {HTMLElement} elt | ||||
|      * @param {number} respCode | ||||
|      * @returns {HTMLElement | null} | ||||
|      */ | ||||
|     function getRespCodeTarget(elt, respCodeNumber) { | ||||
|         if (!elt || !respCodeNumber) return null; | ||||
|  | ||||
|         var respCode = respCodeNumber.toString(); | ||||
|  | ||||
|         // '*' is the original syntax, as the obvious character for a wildcard. | ||||
|         // The 'x' alternative was added for maximum compatibility with HTML | ||||
|         // templating engines, due to ambiguity around which characters are | ||||
|         // supported in HTML attributes. | ||||
|         // | ||||
|         // Start with the most specific possible attribute and generalize from | ||||
|         // there. | ||||
|         var attrPossibilities = [ | ||||
|             respCode, | ||||
|  | ||||
|             respCode.substr(0, 2) + '*', | ||||
|             respCode.substr(0, 2) + 'x', | ||||
|  | ||||
|             respCode.substr(0, 1) + '*', | ||||
|             respCode.substr(0, 1) + 'x', | ||||
|             respCode.substr(0, 1) + '**', | ||||
|             respCode.substr(0, 1) + 'xx', | ||||
|  | ||||
|             '*', | ||||
|             'x', | ||||
|             '***', | ||||
|             'xxx', | ||||
|         ]; | ||||
|         if (startsWith(respCode, '4') || startsWith(respCode, '5')) { | ||||
|             attrPossibilities.push('error'); | ||||
|         } | ||||
|  | ||||
|         for (var i = 0; i < attrPossibilities.length; i++) { | ||||
|             var attr = attrPrefix + attrPossibilities[i]; | ||||
|             var attrValue = api.getClosestAttributeValue(elt, attr); | ||||
|             if (attrValue) { | ||||
|                 if (attrValue === "this") { | ||||
|                     return api.findThisElement(elt, attr); | ||||
|                 } else { | ||||
|                     return api.querySelectorExt(elt, attrValue); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** @param {Event} evt */ | ||||
|     function handleErrorFlag(evt) { | ||||
|         if (evt.detail.isError) { | ||||
|             if (htmx.config.responseTargetUnsetsError) { | ||||
|                 evt.detail.isError = false; | ||||
|             } | ||||
|         } else if (htmx.config.responseTargetSetsError) { | ||||
|             evt.detail.isError = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     htmx.defineExtension('response-targets', { | ||||
|  | ||||
|         /** @param {import("../htmx").HtmxInternalApi} apiRef */ | ||||
|         init: function (apiRef) { | ||||
|             api = apiRef; | ||||
|  | ||||
|             if (htmx.config.responseTargetUnsetsError === undefined) { | ||||
|                 htmx.config.responseTargetUnsetsError = true; | ||||
|             } | ||||
|             if (htmx.config.responseTargetSetsError === undefined) { | ||||
|                 htmx.config.responseTargetSetsError = false; | ||||
|             } | ||||
|             if (htmx.config.responseTargetPrefersExisting === undefined) { | ||||
|                 htmx.config.responseTargetPrefersExisting = false; | ||||
|             } | ||||
|             if (htmx.config.responseTargetPrefersRetargetHeader === undefined) { | ||||
|                 htmx.config.responseTargetPrefersRetargetHeader = true; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * @param {string} name | ||||
|          * @param {Event} evt | ||||
|          */ | ||||
|         onEvent: function (name, evt) { | ||||
|             if (name === "htmx:beforeSwap"    && | ||||
|                 evt.detail.xhr                && | ||||
|                 evt.detail.xhr.status !== 200) { | ||||
|                 if (evt.detail.target) { | ||||
|                     if (htmx.config.responseTargetPrefersExisting) { | ||||
|                         evt.detail.shouldSwap = true; | ||||
|                         handleErrorFlag(evt); | ||||
|                         return true; | ||||
|                     } | ||||
|                     if (htmx.config.responseTargetPrefersRetargetHeader && | ||||
|                         evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) { | ||||
|                         evt.detail.shouldSwap = true; | ||||
|                         handleErrorFlag(evt); | ||||
|                         return true; | ||||
|                     } | ||||
|                 } | ||||
|                 if (!evt.detail.requestConfig) { | ||||
|                     return true; | ||||
|                 } | ||||
|                 var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status); | ||||
|                 if (target) { | ||||
|                     handleErrorFlag(evt); | ||||
|                     evt.detail.shouldSwap = true; | ||||
|                     evt.detail.target = target; | ||||
|                 } | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| })(); | ||||
							
								
								
									
										1
									
								
								views/public/js/htmx.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										23
									
								
								views/public/js/include-vals.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| (function() { | ||||
|   function mergeObjects(obj1, obj2) { | ||||
|     for (var key in obj2) { | ||||
|       if (obj2.hasOwnProperty(key)) { | ||||
|         obj1[key] = obj2[key] | ||||
|       } | ||||
|     } | ||||
|     return obj1 | ||||
|   } | ||||
|  | ||||
|   htmx.defineExtension('include-vals', { | ||||
|     onEvent: function(name, evt) { | ||||
|       if (name === 'htmx:configRequest') { | ||||
|         var includeValsElt = htmx.closest(evt.detail.elt, '[include-vals],[data-include-vals]') | ||||
|         if (includeValsElt) { | ||||
|           var includeVals = includeValsElt.getAttribute('include-vals') || includeValsElt.getAttribute('data-include-vals') | ||||
|           var valuesToInclude = eval('({' + includeVals + '})') | ||||
|           mergeObjects(evt.detail.parameters, valuesToInclude) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| })() | ||||
							
								
								
									
										184
									
								
								views/public/js/loading-states.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,184 @@ | ||||
| ;(function() { | ||||
|   const loadingStatesUndoQueue = [] | ||||
|  | ||||
|   function loadingStateContainer(target) { | ||||
|     return htmx.closest(target, '[data-loading-states]') || document.body | ||||
|   } | ||||
|  | ||||
|   function mayProcessUndoCallback(target, callback) { | ||||
|     if (document.body.contains(target)) { | ||||
|       callback() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function mayProcessLoadingStateByPath(elt, requestPath) { | ||||
|     const pathElt = htmx.closest(elt, '[data-loading-path]') | ||||
|     if (!pathElt) { | ||||
|       return true | ||||
|     } | ||||
|  | ||||
|     return pathElt.getAttribute('data-loading-path') === requestPath | ||||
|   } | ||||
|  | ||||
|   function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) { | ||||
|     const delayElt = htmx.closest(sourceElt, '[data-loading-delay]') | ||||
|     if (delayElt) { | ||||
|       const delayInMilliseconds = | ||||
|         delayElt.getAttribute('data-loading-delay') || 200 | ||||
|       const timeout = setTimeout(function() { | ||||
|         doCallback() | ||||
|  | ||||
|         loadingStatesUndoQueue.push(function() { | ||||
|           mayProcessUndoCallback(targetElt, undoCallback) | ||||
|         }) | ||||
|       }, delayInMilliseconds) | ||||
|  | ||||
|       loadingStatesUndoQueue.push(function() { | ||||
|         mayProcessUndoCallback(targetElt, function() { clearTimeout(timeout) }) | ||||
|       }) | ||||
|     } else { | ||||
|       doCallback() | ||||
|       loadingStatesUndoQueue.push(function() { | ||||
|         mayProcessUndoCallback(targetElt, undoCallback) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function getLoadingStateElts(loadingScope, type, path) { | ||||
|     return Array.from(htmx.findAll(loadingScope, '[' + type + ']')).filter( | ||||
|       function(elt) { return mayProcessLoadingStateByPath(elt, path) } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   function getLoadingTarget(elt) { | ||||
|     if (elt.getAttribute('data-loading-target')) { | ||||
|       return Array.from( | ||||
|         htmx.findAll(elt.getAttribute('data-loading-target')) | ||||
|       ) | ||||
|     } | ||||
|     return [elt] | ||||
|   } | ||||
|  | ||||
|   htmx.defineExtension('loading-states', { | ||||
|     onEvent: function(name, evt) { | ||||
|       if (name === 'htmx:beforeRequest') { | ||||
|         const container = loadingStateContainer(evt.target) | ||||
|  | ||||
|         const loadingStateTypes = [ | ||||
|           'data-loading', | ||||
|           'data-loading-class', | ||||
|           'data-loading-class-remove', | ||||
|           'data-loading-disable', | ||||
|           'data-loading-aria-busy' | ||||
|         ] | ||||
|  | ||||
|         const loadingStateEltsByType = {} | ||||
|  | ||||
|         loadingStateTypes.forEach(function(type) { | ||||
|           loadingStateEltsByType[type] = getLoadingStateElts( | ||||
|             container, | ||||
|             type, | ||||
|             evt.detail.pathInfo.requestPath | ||||
|           ) | ||||
|         }) | ||||
|  | ||||
|         loadingStateEltsByType['data-loading'].forEach(function(sourceElt) { | ||||
|           getLoadingTarget(sourceElt).forEach(function(targetElt) { | ||||
|             queueLoadingState( | ||||
|               sourceElt, | ||||
|               targetElt, | ||||
|               function() { | ||||
|                 targetElt.style.display = | ||||
|                   sourceElt.getAttribute('data-loading') || | ||||
|                   'inline-block' | ||||
|               }, | ||||
|               function() { targetElt.style.display = 'none' } | ||||
|             ) | ||||
|           }) | ||||
|         }) | ||||
|  | ||||
|         loadingStateEltsByType['data-loading-class'].forEach( | ||||
|           function(sourceElt) { | ||||
|             const classNames = sourceElt | ||||
|               .getAttribute('data-loading-class') | ||||
|               .split(' ') | ||||
|  | ||||
|             getLoadingTarget(sourceElt).forEach(function(targetElt) { | ||||
|               queueLoadingState( | ||||
|                 sourceElt, | ||||
|                 targetElt, | ||||
|                 function() { | ||||
|                   classNames.forEach(function(className) { | ||||
|                     targetElt.classList.add(className) | ||||
|                   }) | ||||
|                 }, | ||||
|                 function() { | ||||
|                   classNames.forEach(function(className) { | ||||
|                     targetElt.classList.remove(className) | ||||
|                   }) | ||||
|                 } | ||||
|               ) | ||||
|             }) | ||||
|           } | ||||
|         ) | ||||
|  | ||||
|         loadingStateEltsByType['data-loading-class-remove'].forEach( | ||||
|           function(sourceElt) { | ||||
|             const classNames = sourceElt | ||||
|               .getAttribute('data-loading-class-remove') | ||||
|               .split(' ') | ||||
|  | ||||
|             getLoadingTarget(sourceElt).forEach(function(targetElt) { | ||||
|               queueLoadingState( | ||||
|                 sourceElt, | ||||
|                 targetElt, | ||||
|                 function() { | ||||
|                   classNames.forEach(function(className) { | ||||
|                     targetElt.classList.remove(className) | ||||
|                   }) | ||||
|                 }, | ||||
|                 function() { | ||||
|                   classNames.forEach(function(className) { | ||||
|                     targetElt.classList.add(className) | ||||
|                   }) | ||||
|                 } | ||||
|               ) | ||||
|             }) | ||||
|           } | ||||
|         ) | ||||
|  | ||||
|         loadingStateEltsByType['data-loading-disable'].forEach( | ||||
|           function(sourceElt) { | ||||
|             getLoadingTarget(sourceElt).forEach(function(targetElt) { | ||||
|               queueLoadingState( | ||||
|                 sourceElt, | ||||
|                 targetElt, | ||||
|                 function() { targetElt.disabled = true }, | ||||
|                 function() { targetElt.disabled = false } | ||||
|               ) | ||||
|             }) | ||||
|           } | ||||
|         ) | ||||
|  | ||||
|         loadingStateEltsByType['data-loading-aria-busy'].forEach( | ||||
|           function(sourceElt) { | ||||
|             getLoadingTarget(sourceElt).forEach(function(targetElt) { | ||||
|               queueLoadingState( | ||||
|                 sourceElt, | ||||
|                 targetElt, | ||||
|                 function() { targetElt.setAttribute('aria-busy', 'true') }, | ||||
|                 function() { targetElt.removeAttribute('aria-busy') } | ||||
|               ) | ||||
|             }) | ||||
|           } | ||||
|         ) | ||||
|       } | ||||
|  | ||||
|       if (name === 'htmx:beforeOnLoad') { | ||||
|         while (loadingStatesUndoQueue.length > 0) { | ||||
|           loadingStatesUndoQueue.shift()() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| })() | ||||
							
								
								
									
										44
									
								
								views/public/js/multi-swap.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| (function() { | ||||
|   /** @type {import("../htmx").HtmxInternalApi} */ | ||||
|   var api | ||||
|  | ||||
|   htmx.defineExtension('multi-swap', { | ||||
|     init: function(apiRef) { | ||||
|       api = apiRef | ||||
|     }, | ||||
|     isInlineSwap: function(swapStyle) { | ||||
|       return swapStyle.indexOf('multi:') === 0 | ||||
|     }, | ||||
|     handleSwap: function(swapStyle, target, fragment, settleInfo) { | ||||
|       if (swapStyle.indexOf('multi:') === 0) { | ||||
|         var selectorToSwapStyle = {} | ||||
|         var elements = swapStyle.replace(/^multi\s*:\s*/, '').split(/\s*,\s*/) | ||||
|  | ||||
|         elements.forEach(function(element) { | ||||
|           var split = element.split(/\s*:\s*/) | ||||
|           var elementSelector = split[0] | ||||
|           var elementSwapStyle = typeof (split[1]) !== 'undefined' ? split[1] : 'innerHTML' | ||||
|  | ||||
|           if (elementSelector.charAt(0) !== '#') { | ||||
|             console.error("HTMX multi-swap: unsupported selector '" + elementSelector + "'. Only ID selectors starting with '#' are supported.") | ||||
|             return | ||||
|           } | ||||
|  | ||||
|           selectorToSwapStyle[elementSelector] = elementSwapStyle | ||||
|         }) | ||||
|  | ||||
|         for (var selector in selectorToSwapStyle) { | ||||
|           var swapStyle = selectorToSwapStyle[selector] | ||||
|           var elementToSwap = fragment.querySelector(selector) | ||||
|           if (elementToSwap) { | ||||
|             api.oobSwap(swapStyle, elementToSwap, settleInfo) | ||||
|           } else { | ||||
|             console.warn("HTMX multi-swap: selector '" + selector + "' not found in source content.") | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         return true | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| })() | ||||
							
								
								
									
										11
									
								
								views/public/js/path-params.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| htmx.defineExtension('path-params', { | ||||
|   onEvent: function(name, evt) { | ||||
|     if (name === 'htmx:configRequest') { | ||||
|       evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function(_, param) { | ||||
|         var val = evt.detail.parameters[param] | ||||
|         delete evt.detail.parameters[param] | ||||
|         return val === undefined ? '{' + param + '}' : encodeURIComponent(val) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| }) | ||||
							
								
								
									
										388
									
								
								views/public/js/preload.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,388 @@ | ||||
| (function() { | ||||
|   /** | ||||
|    * This adds the "preload" extension to htmx. The extension will  | ||||
|     * preload the targets of elements with "preload" attribute if: | ||||
|     * - they also have `href`, `hx-get` or `data-hx-get` attributes | ||||
|     * - they are radio buttons, checkboxes, select elements and submit | ||||
|     *   buttons of forms with `method="get"` or `hx-get` attributes | ||||
|     * The extension relies on browser cache and for it to work | ||||
|     * server response must include `Cache-Control` header | ||||
|     * e.g. `Cache-Control: private, max-age=60`. | ||||
|     * For more details @see https://htmx.org/extensions/preload/ | ||||
|   */ | ||||
|  | ||||
|   htmx.defineExtension('preload', { | ||||
|     onEvent: function(name, event) { | ||||
|       // Process preload attributes on `htmx:afterProcessNode` | ||||
|       if (name === 'htmx:afterProcessNode') { | ||||
|         // Initialize all nodes with `preload` attribute | ||||
|         const parent = event.target || event.detail.elt; | ||||
|         const preloadNodes = [ | ||||
|           ...parent.hasAttribute("preload") ? [parent] : [], | ||||
|           ...parent.querySelectorAll("[preload]")] | ||||
|         preloadNodes.forEach(function(node) { | ||||
|           // Initialize the node with the `preload` attribute | ||||
|           init(node) | ||||
|  | ||||
|           // Initialize all child elements which has | ||||
|           // `href`, `hx-get` or `data-hx-get` attributes | ||||
|           node.querySelectorAll('[href],[hx-get],[data-hx-get]').forEach(init) | ||||
|         }) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       // Intercept HTMX preload requests on `htmx:beforeRequest` and | ||||
|       // send them as XHR requests instead to avoid side-effects, | ||||
|       // such as showing loading indicators while preloading data.  | ||||
|       if (name === 'htmx:beforeRequest') { | ||||
|         const requestHeaders = event.detail.requestConfig.headers | ||||
|         if (!("HX-Preloaded" in requestHeaders | ||||
|               && requestHeaders["HX-Preloaded"] === "true")) { | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         event.preventDefault() | ||||
|         // Reuse XHR created by HTMX with replaced callbacks | ||||
|         const xhr = event.detail.xhr | ||||
|         xhr.onload = function() { | ||||
|           processResponse(event.detail.elt, xhr.responseText) | ||||
|         } | ||||
|         xhr.onerror = null | ||||
|         xhr.onabort = null | ||||
|         xhr.ontimeout = null | ||||
|         xhr.send() | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * Initialize `node`, set up event handlers based on own or inherited | ||||
|    * `preload` attributes and set `node.preloadState` to `READY`. | ||||
|    *  | ||||
|    * `node.preloadState` can have these values: | ||||
|    * - `READY` - event handlers have been set up and node is ready to preload | ||||
|    * - `TIMEOUT` - a triggering event has been fired, but `node` is not | ||||
|    *   yet being loaded because some time need to pass first e.g. user | ||||
|    *   has to keep hovering over an element for 100ms for preload to start | ||||
|    * - `LOADING` means that `node` is in the process of being preloaded | ||||
|    * - `DONE` means that the preloading process is complete and `node` | ||||
|    *    doesn't need a repeated preload (indicated by preload="always") | ||||
|    * @param {Node} node | ||||
|    */ | ||||
|   function init(node) { | ||||
|     // Guarantee that each node is initialized only once | ||||
|     if (node.preloadState !== undefined) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     if (!isValidNodeForPreloading(node)) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Initialize form element preloading | ||||
|     if (node instanceof HTMLFormElement) { | ||||
|       const form = node | ||||
|       // Only initialize forms with `method="get"` or `hx-get` attributes | ||||
|       if (!((form.hasAttribute('method') && form.method === 'get') | ||||
|         || form.hasAttribute('hx-get') || form.hasAttribute('hx-data-get'))) { | ||||
|         return | ||||
|       } | ||||
|       for (let i = 0; i < form.elements.length; i++) { | ||||
|         const element = form.elements.item(i); | ||||
|         init(element); | ||||
|         element.labels.forEach(init); | ||||
|       } | ||||
|       return | ||||
|     } | ||||
|      | ||||
|     // Process node configuration from preload attribute | ||||
|     let preloadAttr = getClosestAttribute(node, 'preload'); | ||||
|     node.preloadAlways = preloadAttr && preloadAttr.includes('always'); | ||||
|     if (node.preloadAlways) { | ||||
|       preloadAttr = preloadAttr.replace('always', '').trim(); | ||||
|     } | ||||
|     let triggerEventName = preloadAttr || 'mousedown'; | ||||
|  | ||||
|     // Set up event handlers listening for triggering events | ||||
|     const needsTimeout = triggerEventName === 'mouseover' | ||||
|     node.addEventListener(triggerEventName, getEventHandler(node, needsTimeout)) | ||||
|  | ||||
|     // Add `touchstart` listener for touchscreen support | ||||
|     // if `mousedown` or `mouseover` is used | ||||
|     if (triggerEventName === 'mousedown' || triggerEventName === 'mouseover') { | ||||
|       node.addEventListener('touchstart', getEventHandler(node)) | ||||
|     } | ||||
|  | ||||
|     // If `mouseover` is used, set up `mouseout` listener, | ||||
|     // which will abort preloading if user moves mouse outside | ||||
|     // the element in less than 100ms after hovering over it | ||||
|     if (triggerEventName === 'mouseover') { | ||||
|       node.addEventListener('mouseout', function(evt) { | ||||
|         if ((evt.target === node) && (node.preloadState === 'TIMEOUT')) { | ||||
|           node.preloadState = 'READY' | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     // Mark the node as ready to be preloaded | ||||
|     node.preloadState = 'READY' | ||||
|  | ||||
|     // This event can be used to load content immediately | ||||
|     htmx.trigger(node, 'preload:init') | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Return event handler which can be called by event listener to start | ||||
|    * the preloading process of `node` with or without a timeout | ||||
|    * @param {Node} node  | ||||
|    * @param {boolean=} needsTimeout  | ||||
|    * @returns {function(): void} | ||||
|    */ | ||||
|   function getEventHandler(node, needsTimeout = false) { | ||||
|     return function() { | ||||
|       // Do not preload uninitialized nodes, nodes which are in process | ||||
|       // of being preloaded or have been preloaded and don't need repeat | ||||
|       if (node.preloadState !== 'READY') { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       if (needsTimeout) { | ||||
|         node.preloadState = 'TIMEOUT' | ||||
|         const timeoutMs = 100 | ||||
|         window.setTimeout(function() { | ||||
|           if (node.preloadState === 'TIMEOUT') { | ||||
|             node.preloadState = 'READY' | ||||
|             load(node) | ||||
|           } | ||||
|         }, timeoutMs) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       load(node) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Preload the target of node, which can be: | ||||
|    *  - hx-get or data-hx-get attribute | ||||
|    *  - href or form action attribute | ||||
|    * @param {Node} node  | ||||
|    */ | ||||
|   function load(node) { | ||||
|     // Do not preload uninitialized nodes, nodes which are in process | ||||
|     // of being preloaded or have been preloaded and don't need repeat | ||||
|     if (node.preloadState !== 'READY') { | ||||
|       return | ||||
|     } | ||||
|     node.preloadState = 'LOADING' | ||||
|  | ||||
|     // Load nodes with `hx-get` or `data-hx-get` attribute | ||||
|     // Forms don't reach this because only their elements are initialized | ||||
|     const hxGet = node.getAttribute('hx-get') || node.getAttribute('data-hx-get') | ||||
|     if (hxGet) { | ||||
|       sendHxGetRequest(hxGet, node); | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Load nodes with `href` attribute | ||||
|     const hxBoost = getClosestAttribute(node, "hx-boost") === "true" | ||||
|     if (node.hasAttribute('href')) { | ||||
|       const url = node.getAttribute('href'); | ||||
|       if (hxBoost) { | ||||
|         sendHxGetRequest(url, node); | ||||
|       } else { | ||||
|         sendXmlGetRequest(url, node); | ||||
|       } | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Load form elements | ||||
|     if (isPreloadableFormElement(node)) { | ||||
|       const url = node.form.getAttribute('action') | ||||
|                   || node.form.getAttribute('hx-get') | ||||
|                   || node.form.getAttribute('data-hx-get'); | ||||
|       const formData = htmx.values(node.form); | ||||
|       const isStandardForm = !(node.form.getAttribute('hx-get') | ||||
|                               || node.form.getAttribute('data-hx-get') | ||||
|                               || hxBoost); | ||||
|       const sendGetRequest = isStandardForm ? sendXmlGetRequest : sendHxGetRequest | ||||
|  | ||||
|       // submit button | ||||
|       if (node.type === 'submit') { | ||||
|         sendGetRequest(url, node.form, formData) | ||||
|         return | ||||
|       } | ||||
|        | ||||
|       // select | ||||
|       const inputName = node.name || node.control.name; | ||||
|       if (node.tagName === 'SELECT') { | ||||
|         Array.from(node.options).forEach(option => { | ||||
|           if (option.selected) return; | ||||
|           formData.set(inputName, option.value); | ||||
|           const formDataOrdered = forceFormDataInOrder(node.form, formData); | ||||
|           sendGetRequest(url, node.form, formDataOrdered) | ||||
|         }); | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       // radio and checkbox | ||||
|       const inputType = node.getAttribute("type") || node.control.getAttribute("type"); | ||||
|       const nodeValue = node.value || node.control?.value; | ||||
|       if (inputType === 'radio') { | ||||
|         formData.set(inputName, nodeValue); | ||||
|       } else if (inputType === 'checkbox'){ | ||||
|         const inputValues = formData.getAll(inputName); | ||||
|         if (inputValues.includes(nodeValue)) { | ||||
|           formData[inputName] = inputValues.filter(value => value !== nodeValue); | ||||
|         } else { | ||||
|           formData.append(inputName, nodeValue); | ||||
|         } | ||||
|       } | ||||
|       const formDataOrdered = forceFormDataInOrder(node.form, formData); | ||||
|       sendGetRequest(url, node.form, formDataOrdered) | ||||
|       return | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Force formData values to be in the order of form elements. | ||||
|    * This is useful to apply after alternating formData values | ||||
|    * and before passing them to a HTTP request because cache is | ||||
|    * sensitive to GET parameter order e.g., cached `/link?a=1&b=2` | ||||
|    * will not be used for `/link?b=2&a=1`. | ||||
|    * @param {HTMLFormElement} form  | ||||
|    * @param {FormData} formData  | ||||
|    * @returns {FormData} | ||||
|    */ | ||||
|   function forceFormDataInOrder(form, formData) { | ||||
|     const formElements = form.elements; | ||||
|     const orderedFormData = new FormData(); | ||||
|     for(let i = 0; i < formElements.length; i++) { | ||||
|       const element = formElements.item(i); | ||||
|       if (formData.has(element.name) && element.tagName === 'SELECT') { | ||||
|         orderedFormData.append( | ||||
|           element.name, formData.get(element.name)); | ||||
|         continue; | ||||
|       } | ||||
|       if (formData.has(element.name) && formData.getAll(element.name) | ||||
|         .includes(element.value)) { | ||||
|         orderedFormData.append(element.name, element.value); | ||||
|       } | ||||
|     } | ||||
|     return orderedFormData; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Send GET request with `hx-request` headers as if `sourceNode` | ||||
|    * target was loaded. Send alternated values if `formData` is set. | ||||
|    *  | ||||
|    * Note that this request is intercepted and sent as XMLHttpRequest. | ||||
|    * It is necessary to use `htmx.ajax` to acquire correct headers which | ||||
|    * HTMX and extensions add based on `sourceNode`. But it cannot be used | ||||
|    * to perform the request due to side-effects e.g. loading indicators.  | ||||
|    * @param {string} url  | ||||
|    * @param {Node} sourceNode  | ||||
|    * @param {FormData=} formData | ||||
|    */ | ||||
|   function sendHxGetRequest(url, sourceNode, formData = undefined) { | ||||
|     htmx.ajax('GET', url, { | ||||
|       source: sourceNode, | ||||
|       values: formData, | ||||
|       headers: {"HX-Preloaded": "true"} | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Send XML GET request to `url`. Send `formData` as URL params if set. | ||||
|    * @param {string} url | ||||
|    * @param {Node} sourceNode | ||||
|    * @param {FormData=} formData | ||||
|    */ | ||||
|   function sendXmlGetRequest(url, sourceNode, formData = undefined) { | ||||
|     const xhr = new XMLHttpRequest() | ||||
|     if (formData) { | ||||
|       url += '?' + new URLSearchParams(formData.entries()).toString() | ||||
|     } | ||||
|     xhr.open('GET', url); | ||||
|     xhr.setRequestHeader("HX-Preloaded", "true") | ||||
|     xhr.onload = function() { processResponse(sourceNode, xhr.responseText) } | ||||
|     xhr.send() | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Process request response by marking node `DONE` to prevent repeated | ||||
|    * requests, except if preload attribute contains `always`, | ||||
|    * and load linked resources (e.g. images) returned in the response  | ||||
|    * if `preload-images` attribute is `true` | ||||
|    * @param {Node} node  | ||||
|    * @param {string} responseText  | ||||
|    */ | ||||
|   function processResponse(node, responseText) { | ||||
|     node.preloadState = node.preloadAlways ? 'READY' : 'DONE' | ||||
|  | ||||
|     if (getClosestAttribute(node, 'preload-images') === 'true') { | ||||
|       // Load linked resources | ||||
|       document.createElement('div').innerHTML = responseText | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Gets attribute value from node or one of its parents | ||||
|    * @param {Node} node  | ||||
|    * @param {string} attribute  | ||||
|    * @returns { string | undefined } | ||||
|    */ | ||||
|   function getClosestAttribute(node, attribute) { | ||||
|     if (node == undefined) { return undefined } | ||||
|     return node.getAttribute(attribute) | ||||
|       || node.getAttribute('data-' + attribute) | ||||
|       || getClosestAttribute(node.parentElement, attribute) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Determines if node is valid for preloading and should be | ||||
|    * initialized by setting up event listeners and handlers | ||||
|    * @param {Node} node  | ||||
|    * @returns {boolean} | ||||
|    */ | ||||
|   function isValidNodeForPreloading(node) { | ||||
|     // Add listeners only to nodes which include "GET" transactions | ||||
|     // or preloadable "GET" form elements | ||||
|     const getReqAttrs = ['href', 'hx-get', 'data-hx-get']; | ||||
|     const includesGetRequest = node => getReqAttrs.some(a => node.hasAttribute(a)) | ||||
|                                         || node.method === 'get'; | ||||
|     const isPreloadableGetFormElement = node.form instanceof HTMLFormElement | ||||
|                                         && includesGetRequest(node.form) | ||||
|                                         && isPreloadableFormElement(node) | ||||
|     if (!includesGetRequest(node) && !isPreloadableGetFormElement) { | ||||
|       return false | ||||
|     } | ||||
|  | ||||
|     // Don't preload <input> elements contained in <label> | ||||
|     // to prevent sending two requests. Interaction on <input> in a | ||||
|     // <label><input></input></label> situation activates <label> too. | ||||
|     if (node instanceof HTMLInputElement && node.closest('label')) { | ||||
|       return false | ||||
|     } | ||||
|  | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Determine if node is a form element which can be preloaded, | ||||
|    * i.e., `radio`, `checkbox`, `select` or `submit` button | ||||
|    * or a `label` of a form element which can be preloaded. | ||||
|    * @param {Node} node  | ||||
|    * @returns {boolean} | ||||
|    */ | ||||
|   function isPreloadableFormElement(node) { | ||||
|     if (node instanceof HTMLInputElement || node instanceof HTMLButtonElement) { | ||||
|       const type = node.getAttribute('type'); | ||||
|       return ['checkbox', 'radio', 'submit'].includes(type); | ||||
|     } | ||||
|     if (node instanceof HTMLLabelElement) { | ||||
|       return node.control && isPreloadableFormElement(node.control); | ||||
|     } | ||||
|     return node instanceof HTMLSelectElement; | ||||
|   } | ||||
| })() | ||||
							
								
								
									
										471
									
								
								views/public/js/ws.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,471 @@ | ||||
| /* | ||||
| WebSockets Extension | ||||
| ============================ | ||||
| This extension adds support for WebSockets to htmx.  See /www/extensions/ws.md for usage instructions. | ||||
| */ | ||||
|  | ||||
| (function() { | ||||
|   /** @type {import("../htmx").HtmxInternalApi} */ | ||||
|   var api | ||||
|  | ||||
|   htmx.defineExtension('ws', { | ||||
|  | ||||
|     /** | ||||
|      * init is called once, when this extension is first registered. | ||||
|      * @param {import("../htmx").HtmxInternalApi} apiRef | ||||
|      */ | ||||
|     init: function(apiRef) { | ||||
|       // Store reference to internal API | ||||
|       api = apiRef | ||||
|  | ||||
|       // Default function for creating new EventSource objects | ||||
|       if (!htmx.createWebSocket) { | ||||
|         htmx.createWebSocket = createWebSocket | ||||
|       } | ||||
|  | ||||
|       // Default setting for reconnect delay | ||||
|       if (!htmx.config.wsReconnectDelay) { | ||||
|         htmx.config.wsReconnectDelay = 'full-jitter' | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * onEvent handles all events passed to this extension. | ||||
|      * | ||||
|      * @param {string} name | ||||
|      * @param {Event} evt | ||||
|      */ | ||||
|     onEvent: function(name, evt) { | ||||
|       var parent = evt.target || evt.detail.elt | ||||
|       switch (name) { | ||||
|         // Try to close the socket when elements are removed | ||||
|         case 'htmx:beforeCleanupElement': | ||||
|  | ||||
|           var internalData = api.getInternalData(parent) | ||||
|  | ||||
|           if (internalData.webSocket) { | ||||
|             internalData.webSocket.close() | ||||
|           } | ||||
|           return | ||||
|  | ||||
|           // Try to create websockets when elements are processed | ||||
|         case 'htmx:beforeProcessNode': | ||||
|  | ||||
|           forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), function(child) { | ||||
|             ensureWebSocket(child) | ||||
|           }) | ||||
|           forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), function(child) { | ||||
|             ensureWebSocketSend(child) | ||||
|           }) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   function splitOnWhitespace(trigger) { | ||||
|     return trigger.trim().split(/\s+/) | ||||
|   } | ||||
|  | ||||
|   function getLegacyWebsocketURL(elt) { | ||||
|     var legacySSEValue = api.getAttributeValue(elt, 'hx-ws') | ||||
|     if (legacySSEValue) { | ||||
|       var values = splitOnWhitespace(legacySSEValue) | ||||
|       for (var i = 0; i < values.length; i++) { | ||||
|         var value = values[i].split(/:(.+)/) | ||||
|         if (value[0] === 'connect') { | ||||
|           return value[1] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * ensureWebSocket creates a new WebSocket on the designated element, using | ||||
|    * the element's "ws-connect" attribute. | ||||
|    * @param {HTMLElement} socketElt | ||||
|    * @returns | ||||
|    */ | ||||
|   function ensureWebSocket(socketElt) { | ||||
|     // If the element containing the WebSocket connection no longer exists, then | ||||
|     // do not connect/reconnect the WebSocket. | ||||
|     if (!api.bodyContains(socketElt)) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Get the source straight from the element's value | ||||
|     var wssSource = api.getAttributeValue(socketElt, 'ws-connect') | ||||
|  | ||||
|     if (wssSource == null || wssSource === '') { | ||||
|       var legacySource = getLegacyWebsocketURL(socketElt) | ||||
|       if (legacySource == null) { | ||||
|         return | ||||
|       } else { | ||||
|         wssSource = legacySource | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Guarantee that the wssSource value is a fully qualified URL | ||||
|     if (wssSource.indexOf('/') === 0) { | ||||
|       var base_part = location.hostname + (location.port ? ':' + location.port : '') | ||||
|       if (location.protocol === 'https:') { | ||||
|         wssSource = 'wss://' + base_part + wssSource | ||||
|       } else if (location.protocol === 'http:') { | ||||
|         wssSource = 'ws://' + base_part + wssSource | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     var socketWrapper = createWebsocketWrapper(socketElt, function() { | ||||
|       return htmx.createWebSocket(wssSource) | ||||
|     }) | ||||
|  | ||||
|     socketWrapper.addEventListener('message', function(event) { | ||||
|       if (maybeCloseWebSocketSource(socketElt)) { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       var response = event.data | ||||
|       if (!api.triggerEvent(socketElt, 'htmx:wsBeforeMessage', { | ||||
|         message: response, | ||||
|         socketWrapper: socketWrapper.publicInterface | ||||
|       })) { | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       api.withExtensions(socketElt, function(extension) { | ||||
|         response = extension.transformResponse(response, null, socketElt) | ||||
|       }) | ||||
|  | ||||
|       var settleInfo = api.makeSettleInfo(socketElt) | ||||
|       var fragment = api.makeFragment(response) | ||||
|  | ||||
|       if (fragment.children.length) { | ||||
|         var children = Array.from(fragment.children) | ||||
|         for (var i = 0; i < children.length; i++) { | ||||
|           api.oobSwap(api.getAttributeValue(children[i], 'hx-swap-oob') || 'true', children[i], settleInfo) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       api.settleImmediately(settleInfo.tasks) | ||||
|       api.triggerEvent(socketElt, 'htmx:wsAfterMessage', { message: response, socketWrapper: socketWrapper.publicInterface }) | ||||
|     }) | ||||
|  | ||||
|     // Put the WebSocket into the HTML Element's custom data. | ||||
|     api.getInternalData(socketElt).webSocket = socketWrapper | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @typedef {Object} WebSocketWrapper | ||||
|    * @property {WebSocket} socket | ||||
|    * @property {Array<{message: string, sendElt: Element}>} messageQueue | ||||
|    * @property {number} retryCount | ||||
|    * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state | ||||
|    * @property {(message: string, sendElt: Element) => void} send | ||||
|    * @property {(event: string, handler: Function) => void} addEventListener | ||||
|    * @property {() => void} handleQueuedMessages | ||||
|    * @property {() => void} init | ||||
|    * @property {() => void} close | ||||
|    */ | ||||
|   /** | ||||
|    * | ||||
|    * @param socketElt | ||||
|    * @param socketFunc | ||||
|    * @returns {WebSocketWrapper} | ||||
|    */ | ||||
|   function createWebsocketWrapper(socketElt, socketFunc) { | ||||
|     var wrapper = { | ||||
|       socket: null, | ||||
|       messageQueue: [], | ||||
|       retryCount: 0, | ||||
|  | ||||
|       /** @type {Object<string, Function[]>} */ | ||||
|       events: {}, | ||||
|  | ||||
|       addEventListener: function(event, handler) { | ||||
|         if (this.socket) { | ||||
|           this.socket.addEventListener(event, handler) | ||||
|         } | ||||
|  | ||||
|         if (!this.events[event]) { | ||||
|           this.events[event] = [] | ||||
|         } | ||||
|  | ||||
|         this.events[event].push(handler) | ||||
|       }, | ||||
|  | ||||
|       sendImmediately: function(message, sendElt) { | ||||
|         if (!this.socket) { | ||||
|           api.triggerErrorEvent() | ||||
|         } | ||||
|         if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', { | ||||
|           message, | ||||
|           socketWrapper: this.publicInterface | ||||
|         })) { | ||||
|           this.socket.send(message) | ||||
|           sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', { | ||||
|             message, | ||||
|             socketWrapper: this.publicInterface | ||||
|           }) | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       send: function(message, sendElt) { | ||||
|         if (this.socket.readyState !== this.socket.OPEN) { | ||||
|           this.messageQueue.push({ message, sendElt }) | ||||
|         } else { | ||||
|           this.sendImmediately(message, sendElt) | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       handleQueuedMessages: function() { | ||||
|         while (this.messageQueue.length > 0) { | ||||
|           var queuedItem = this.messageQueue[0] | ||||
|           if (this.socket.readyState === this.socket.OPEN) { | ||||
|             this.sendImmediately(queuedItem.message, queuedItem.sendElt) | ||||
|             this.messageQueue.shift() | ||||
|           } else { | ||||
|             break | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       init: function() { | ||||
|         if (this.socket && this.socket.readyState === this.socket.OPEN) { | ||||
|           // Close discarded socket | ||||
|           this.socket.close() | ||||
|         } | ||||
|  | ||||
|         // Create a new WebSocket and event handlers | ||||
|         /** @type {WebSocket} */ | ||||
|         var socket = socketFunc() | ||||
|  | ||||
|         // The event.type detail is added for interface conformance with the | ||||
|         // other two lifecycle events (open and close) so a single handler method | ||||
|         // can handle them polymorphically, if required. | ||||
|         api.triggerEvent(socketElt, 'htmx:wsConnecting', { event: { type: 'connecting' } }) | ||||
|  | ||||
|         this.socket = socket | ||||
|  | ||||
|         socket.onopen = function(e) { | ||||
|           wrapper.retryCount = 0 | ||||
|           api.triggerEvent(socketElt, 'htmx:wsOpen', { event: e, socketWrapper: wrapper.publicInterface }) | ||||
|           wrapper.handleQueuedMessages() | ||||
|         } | ||||
|  | ||||
|         socket.onclose = function(e) { | ||||
|           // If socket should not be connected, stop further attempts to establish connection | ||||
|           // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause. | ||||
|           if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) { | ||||
|             var delay = getWebSocketReconnectDelay(wrapper.retryCount) | ||||
|             setTimeout(function() { | ||||
|               wrapper.retryCount += 1 | ||||
|               wrapper.init() | ||||
|             }, delay) | ||||
|           } | ||||
|  | ||||
|           // Notify client code that connection has been closed. Client code can inspect `event` field | ||||
|           // to determine whether closure has been valid or abnormal | ||||
|           api.triggerEvent(socketElt, 'htmx:wsClose', { event: e, socketWrapper: wrapper.publicInterface }) | ||||
|         } | ||||
|  | ||||
|         socket.onerror = function(e) { | ||||
|           api.triggerErrorEvent(socketElt, 'htmx:wsError', { error: e, socketWrapper: wrapper }) | ||||
|           maybeCloseWebSocketSource(socketElt) | ||||
|         } | ||||
|  | ||||
|         var events = this.events | ||||
|         Object.keys(events).forEach(function(k) { | ||||
|           events[k].forEach(function(e) { | ||||
|             socket.addEventListener(k, e) | ||||
|           }) | ||||
|         }) | ||||
|       }, | ||||
|  | ||||
|       close: function() { | ||||
|         this.socket.close() | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     wrapper.init() | ||||
|  | ||||
|     wrapper.publicInterface = { | ||||
|       send: wrapper.send.bind(wrapper), | ||||
|       sendImmediately: wrapper.sendImmediately.bind(wrapper), | ||||
|       queue: wrapper.messageQueue | ||||
|     } | ||||
|  | ||||
|     return wrapper | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * ensureWebSocketSend attaches trigger handles to elements with | ||||
|    * "ws-send" attribute | ||||
|    * @param {HTMLElement} elt | ||||
|    */ | ||||
|   function ensureWebSocketSend(elt) { | ||||
|     var legacyAttribute = api.getAttributeValue(elt, 'hx-ws') | ||||
|     if (legacyAttribute && legacyAttribute !== 'send') { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     var webSocketParent = api.getClosestMatch(elt, hasWebSocket) | ||||
|     processWebSocketSend(webSocketParent, elt) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * hasWebSocket function checks if a node has webSocket instance attached | ||||
|    * @param {HTMLElement} node | ||||
|    * @returns {boolean} | ||||
|    */ | ||||
|   function hasWebSocket(node) { | ||||
|     return api.getInternalData(node).webSocket != null | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * processWebSocketSend adds event listeners to the <form> element so that | ||||
|    * messages can be sent to the WebSocket server when the form is submitted. | ||||
|    * @param {HTMLElement} socketElt | ||||
|    * @param {HTMLElement} sendElt | ||||
|    */ | ||||
|   function processWebSocketSend(socketElt, sendElt) { | ||||
|     var nodeData = api.getInternalData(sendElt) | ||||
|     var triggerSpecs = api.getTriggerSpecs(sendElt) | ||||
|     triggerSpecs.forEach(function(ts) { | ||||
|       api.addTriggerHandler(sendElt, ts, nodeData, function(elt, evt) { | ||||
|         if (maybeCloseWebSocketSource(socketElt)) { | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         /** @type {WebSocketWrapper} */ | ||||
|         var socketWrapper = api.getInternalData(socketElt).webSocket | ||||
|         var headers = api.getHeaders(sendElt, api.getTarget(sendElt)) | ||||
|         var results = api.getInputValues(sendElt, 'post') | ||||
|         var errors = results.errors | ||||
|         var rawParameters = Object.assign({}, results.values) | ||||
|         var expressionVars = api.getExpressionVars(sendElt) | ||||
|         var allParameters = api.mergeObjects(rawParameters, expressionVars) | ||||
|         var filteredParameters = api.filterValues(allParameters, sendElt) | ||||
|  | ||||
|         var sendConfig = { | ||||
|           parameters: filteredParameters, | ||||
|           unfilteredParameters: allParameters, | ||||
|           headers, | ||||
|           errors, | ||||
|  | ||||
|           triggeringEvent: evt, | ||||
|           messageBody: undefined, | ||||
|           socketWrapper: socketWrapper.publicInterface | ||||
|         } | ||||
|  | ||||
|         if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) { | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         if (errors && errors.length > 0) { | ||||
|           api.triggerEvent(elt, 'htmx:validation:halted', errors) | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         var body = sendConfig.messageBody | ||||
|         if (body === undefined) { | ||||
|           var toSend = Object.assign({}, sendConfig.parameters) | ||||
|           if (sendConfig.headers) { toSend.HEADERS = headers } | ||||
|           body = JSON.stringify(toSend) | ||||
|         } | ||||
|  | ||||
|         socketWrapper.send(body, elt) | ||||
|  | ||||
|         if (evt && api.shouldCancel(evt, elt)) { | ||||
|           evt.preventDefault() | ||||
|         } | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * getWebSocketReconnectDelay is the default easing function for WebSocket reconnects. | ||||
|    * @param {number} retryCount // The number of retries that have already taken place | ||||
|    * @returns {number} | ||||
|    */ | ||||
|   function getWebSocketReconnectDelay(retryCount) { | ||||
|     /** @type {"full-jitter" | ((retryCount:number) => number)} */ | ||||
|     var delay = htmx.config.wsReconnectDelay | ||||
|     if (typeof delay === 'function') { | ||||
|       return delay(retryCount) | ||||
|     } | ||||
|     if (delay === 'full-jitter') { | ||||
|       var exp = Math.min(retryCount, 6) | ||||
|       var maxDelay = 1000 * Math.pow(2, exp) | ||||
|       return maxDelay * Math.random() | ||||
|     } | ||||
|  | ||||
|     logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"') | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * maybeCloseWebSocketSource checks to the if the element that created the WebSocket | ||||
|    * still exists in the DOM.  If NOT, then the WebSocket is closed and this function | ||||
|    * returns TRUE.  If the element DOES EXIST, then no action is taken, and this function | ||||
|    * returns FALSE. | ||||
|    * | ||||
|    * @param {*} elt | ||||
|    * @returns | ||||
|    */ | ||||
|   function maybeCloseWebSocketSource(elt) { | ||||
|     if (!api.bodyContains(elt)) { | ||||
|       var internalData = api.getInternalData(elt) | ||||
|       if (internalData.webSocket) { | ||||
|         internalData.webSocket.close() | ||||
|         return true | ||||
|       } | ||||
|       return false | ||||
|     } | ||||
|     return false | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * createWebSocket is the default method for creating new WebSocket objects. | ||||
|    * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed. | ||||
|    * | ||||
|    * @param {string} url | ||||
|    * @returns WebSocket | ||||
|    */ | ||||
|   function createWebSocket(url) { | ||||
|     var sock = new WebSocket(url, []) | ||||
|     sock.binaryType = htmx.config.wsBinaryType | ||||
|     return sock | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. | ||||
|    * | ||||
|    * @param {HTMLElement} elt | ||||
|    * @param {string} attributeName | ||||
|    */ | ||||
|   function queryAttributeOnThisOrChildren(elt, attributeName) { | ||||
|     var result = [] | ||||
|  | ||||
|     // If the parent element also contains the requested attribute, then add it to the results too. | ||||
|     if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, 'hx-ws')) { | ||||
|       result.push(elt) | ||||
|     } | ||||
|  | ||||
|     // Search all child nodes that match the requested attribute | ||||
|     elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + '], [data-hx-ws], [hx-ws]').forEach(function(node) { | ||||
|       result.push(node) | ||||
|     }) | ||||
|  | ||||
|     return result | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @template T | ||||
|    * @param {T[]} arr | ||||
|    * @param {(T) => void} func | ||||
|    */ | ||||
|   function forEach(arr, func) { | ||||
|     if (arr) { | ||||
|       for (var i = 0; i < arr.length; i++) { | ||||
|         func(arr[i]) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| })() | ||||
							
								
								
									
										
											BIN
										
									
								
								views/public/logo/dev_favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 39 KiB | 
							
								
								
									
										
											BIN
										
									
								
								views/public/logo/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										13
									
								
								views/public/xslt/transform-citation.xsl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> | ||||
| 	<xsl:output method="html" indent="yes" /> | ||||
| 	<xsl:template match="title"> | ||||
| 		<em> | ||||
| 			<xsl:apply-templates /> | ||||
| 		</em> | ||||
| 	</xsl:template> | ||||
| 	<xsl:template match="year"> | ||||
| 		<span class=""> | ||||
| 			<xsl:apply-templates /> | ||||
| 		</span> | ||||
| 	</xsl:template> | ||||
| </xsl:stylesheet> | ||||
 Simon Martens
					Simon Martens