all repos — h3 @ 7b87c17e6197639f76ce58809cb3f998fd471a84

A tiny, extremely minimalist JavaScript microframework.

Updated docs, added copyrights.
h3rald h3rald@h3rald.com
Tue, 21 Apr 2020 13:30:38 +0200
commit

7b87c17e6197639f76ce58809cb3f998fd471a84

parent

f0ff583ed1fdfd0ae81f6e856f3b750135cc0bef

M docs/H3_DeveloperGuide.htmdocs/H3_DeveloperGuide.htm

@@ -7260,9 +7260,10 @@ <li><a href="#Components">Components</a></li>

<li><a href="#Store">Store</a></li> <li><a href="#Modules">Modules</a></li> <li><a href="#Router">Router</a></li> + <li><a href="#Sequence-Diagram">Sequence Diagram</a></li> </ul> </li> - <li><a href="#Usage">Usage</a> + <li><a href="#Tutorial">Tutorial</a> <ul> <li><a href="#Create-a-simple-HTML-file">Create a simple HTML file</a></li> <li><a href="#Create-a-single-page-application">Create a single-page application</a></li>

@@ -7299,6 +7300,7 @@ <li><a href="#Why-the-name?">Why the name?</a></li>

<li><a href="#Why-the-weird-release-labels?">Why the weird release labels?</a></li> <li><a href="#A-brief-history-of-H3">A brief history of H3</a></li> <li><a href="#Credits">Credits</a></li> + <li><a href="#Special-Thanks">Special Thanks</a></li> </ul> </li> </ul>

@@ -7491,8 +7493,48 @@ <p>H3 comes with a very minimal but fully functional URL fragment router. You create your application routes when initializing your application, and you can navigate to them using ordinary <code>href</code> links or programmatically using the <code>h3.navigateTo</code> method.</p>

<p>The current route is always accessible via the <code>h3.route</code> property.</p> -<a name="Usage"></a> -<h2>Usage<a href="#document-top" title="Go to top"></a></h2> +<a name="Sequence-Diagram"></a> +<h3>Sequence Diagram<a href="#document-top" title="Go to top"></a></h3> + +<p>The following sequence diagram summarizes how H3 works, from its initialization to the redraw and navigation phases.</p> + +<p><img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U2NyaXB0VHlwZT0iYXBwbGljYXRpb24vZWNtYXNjcmlwdCIgY29udGVudFN0eWxlVHlwZT0idGV4dC9jc3MiIGhlaWdodD0iODgwcHgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiIHN0eWxlPSJ3aWR0aDo3ODdweDtoZWlnaHQ6ODgwcHg7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA3ODcgODgwIiB3aWR0aD0iNzg3cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzPjxmaWx0ZXIgaGVpZ2h0PSIzMDAlIiBpZD0iZjh2eHV6Y3h2dHV1NSIgd2lkdGg9IjMwMCUiIHg9Ii0xIiB5PSItMSI+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iYmx1ck91dCIgc3RkRGV2aWF0aW9uPSIyLjAiLz48ZmVDb2xvck1hdHJpeCBpbj0iYmx1ck91dCIgcmVzdWx0PSJibHVyT3V0MiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIC40IDAiLz48ZmVPZmZzZXQgZHg9IjQuMCIgZHk9IjQuMCIgaW49ImJsdXJPdXQyIiByZXN1bHQ9ImJsdXJPdXQzIi8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iYmx1ck91dDMiIG1vZGU9Im5vcm1hbCIvPjwvZmlsdGVyPjwvZGVmcz48Zz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxOCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIyMDgiIHg9IjI4OC43NSIgeT0iMjYuNzA4Ij5IMyBTZXF1ZW5jZSBEaWFncmFtPC90ZXh0PjxyZWN0IGZpbGw9IiNGRkZGRkYiIGZpbHRlcj0idXJsKCNmOHZ4dXpjeHZ0dXU1KSIgaGVpZ2h0PSIxMDQuNTMxMyIgc3R5bGU9InN0cm9rZTogIzAwMDAwMDsgc3Ryb2tlLXdpZHRoOiAyLjA7IiB3aWR0aD0iNTA2LjUiIHg9IjE4Mi41IiB5PSI0NTAuNzEwOSIvPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGZpbHRlcj0idXJsKCNmOHZ4dXpjeHZ0dXU1KSIgaGVpZ2h0PSIyMjEuMDYyNSIgc3R5bGU9InN0cm9rZTogIzAwMDAwMDsgc3Ryb2tlLXdpZHRoOiAyLjA7IiB3aWR0aD0iNjc2IiB4PSIxMyIgeT0iNTY5LjI0MjIiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsgc3Ryb2tlLWRhc2hhcnJheTogNS4wLDUuMDsiIHgxPSI2NCIgeDI9IjY0IiB5MT0iMTAwLjI1IiB5Mj0iODA3LjMwNDciLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsgc3Ryb2tlLWRhc2hhcnJheTogNS4wLDUuMDsiIHgxPSIyMzYuNSIgeDI9IjIzNi41IiB5MT0iMTAwLjI1IiB5Mj0iODA3LjMwNDciLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsgc3Ryb2tlLWRhc2hhcnJheTogNS4wLDUuMDsiIHgxPSIzNzkuNSIgeDI9IjM3OS41IiB5MT0iMTAwLjI1IiB5Mj0iODA3LjMwNDciLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsgc3Ryb2tlLWRhc2hhcnJheTogNS4wLDUuMDsiIHgxPSI0OTMiIHgyPSI0OTMiIHkxPSIxMDAuMjUiIHkyPSI4MDcuMzA0NyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyBzdHJva2UtZGFzaGFycmF5OiA1LjAsNS4wOyIgeDE9IjY1NiIgeDI9IjY1NiIgeTE9IjEwMC4yNSIgeTI9IjgwNy4zMDQ3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNzciIHg9IjIzIiB5PSI5Ni45NDgyIj5BcHBsaWNhdGlvbjwvdGV4dD48ZWxsaXBzZSBjeD0iNjQuNSIgY3k9IjY3Ljk1MzEiIGZpbGw9IiNGRUZFQ0UiIGZpbHRlcj0idXJsKCNmOHZ4dXpjeHZ0dXU1KSIgcng9IjEyIiByeT0iMTIiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMi4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMi4wOyIgeDE9IjUyLjUiIHgyPSI3Ni41IiB5MT0iODEuOTUzMSIgeTI9IjgxLjk1MzEiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI3NyIgeD0iMjMiIHk9IjgxOS4yOTk4Ij5BcHBsaWNhdGlvbjwvdGV4dD48ZWxsaXBzZSBjeD0iNjQuNSIgY3k9IjgzOC42MDE2IiBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjh2eHV6Y3h2dHV1NSkiIHJ4PSIxMiIgcnk9IjEyIiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiIHgxPSI1Mi41IiB4Mj0iNzYuNSIgeTE9Ijg1Mi42MDE2IiB5Mj0iODUyLjYwMTYiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI4MiIgeD0iMTkyLjUiIHk9Ijk2Ljk0ODIiPkNvbXBvbmVudDwvdGV4dD48ZWxsaXBzZSBjeD0iMjM2LjUiIGN5PSI2Ny45NTMxIiBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjh2eHV6Y3h2dHV1NSkiIHJ4PSIxMiIgcnk9IjEyIiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiLz48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjIzMi41LDU1Ljk1MzEsMjM4LjUsNTAuOTUzMSwyMzYuNSw1NS45NTMxLDIzOC41LDYwLjk1MzEsMjMyLjUsNTUuOTUzMSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iODIiIHg9IjE5Mi41IiB5PSI4MTkuMjk5OCI+Q29tcG9uZW50PC90ZXh0PjxlbGxpcHNlIGN4PSIyMzYuNSIgY3k9IjgzOC42MDE2IiBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjh2eHV6Y3h2dHV1NSkiIHJ4PSIxMiIgcnk9IjEyIiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiLz48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjIzMi41LDgyNi42MDE2LDIzOC41LDgyMS42MDE2LDIzNi41LDgyNi42MDE2LDIzOC41LDgzMS42MDE2LDIzMi41LDgyNi42MDE2IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxOCIgeD0iMzY3LjUiIHk9Ijk2Ljk0ODIiPkgzPC90ZXh0PjxlbGxpcHNlIGN4PSIzNzkuNSIgY3k9IjY3Ljk1MzEiIGZpbGw9IiNGRUZFQ0UiIGZpbHRlcj0idXJsKCNmOHZ4dXpjeHZ0dXU1KSIgcng9IjEyIiByeT0iMTIiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMi4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMi4wOyIgeDE9IjM2Ny41IiB4Mj0iMzkxLjUiIHkxPSI4MS45NTMxIiB5Mj0iODEuOTUzMSIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjE4IiB4PSIzNjcuNSIgeT0iODE5LjI5OTgiPkgzPC90ZXh0PjxlbGxpcHNlIGN4PSIzNzkuNSIgY3k9IjgzOC42MDE2IiBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjh2eHV6Y3h2dHV1NSkiIHJ4PSIxMiIgcnk9IjEyIiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiIHgxPSIzNjcuNSIgeDI9IjM5MS41IiB5MT0iODUyLjYwMTYiIHkyPSI4NTIuNjAxNiIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjQ3IiB4PSI0NjciIHk9Ijk2Ljk0ODIiPlJvdXRlcjwvdGV4dD48ZWxsaXBzZSBjeD0iNDkzLjUiIGN5PSI2Ny45NTMxIiBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjh2eHV6Y3h2dHV1NSkiIHJ4PSIxMiIgcnk9IjEyIiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiIHgxPSI0ODEuNSIgeDI9IjUwNS41IiB5MT0iODEuOTUzMSIgeTI9IjgxLjk1MzEiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI0NyIgeD0iNDY3IiB5PSI4MTkuMjk5OCI+Um91dGVyPC90ZXh0PjxlbGxpcHNlIGN4PSI0OTMuNSIgY3k9IjgzOC42MDE2IiBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjh2eHV6Y3h2dHV1NSkiIHJ4PSIxMiIgcnk9IjEyIiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDIuMDsiIHgxPSI0ODEuNSIgeDI9IjUwNS41IiB5MT0iODUyLjYwMTYiIHkyPSI4NTIuNjAxNiIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjM5IiB4PSI2MzQiIHk9Ijk2Ljk0ODIiPlN0b3JlPC90ZXh0PjxwYXRoIGQ9Ik02MzguNSw0Ny45NTMxIEM2MzguNSwzNy45NTMxIDY1Ni41LDM3Ljk1MzEgNjU2LjUsMzcuOTUzMSBDNjU2LjUsMzcuOTUzMSA2NzQuNSwzNy45NTMxIDY3NC41LDQ3Ljk1MzEgTDY3NC41LDczLjk1MzEgQzY3NC41LDgzLjk1MzEgNjU2LjUsODMuOTUzMSA2NTYuNSw4My45NTMxIEM2NTYuNSw4My45NTMxIDYzOC41LDgzLjk1MzEgNjM4LjUsNzMuOTUzMSBMNjM4LjUsNDcuOTUzMSAiIGZpbGw9IiNGRUZFQ0UiIGZpbHRlcj0idXJsKCNmOHZ4dXpjeHZ0dXU1KSIgc3R5bGU9InN0cm9rZTogIzAwMDAwMDsgc3Ryb2tlLXdpZHRoOiAxLjU7Ii8+PHBhdGggZD0iTTYzOC41LDQ3Ljk1MzEgQzYzOC41LDU3Ljk1MzEgNjU2LjUsNTcuOTUzMSA2NTYuNSw1Ny45NTMxIEM2NTYuNSw1Ny45NTMxIDY3NC41LDU3Ljk1MzEgNjc0LjUsNDcuOTUzMSAiIGZpbGw9Im5vbmUiIHN0eWxlPSJzdHJva2U6ICMwMDAwMDA7IHN0cm9rZS13aWR0aDogMS41OyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjM5IiB4PSI2MzQiIHk9IjgxOS4yOTk4Ij5TdG9yZTwvdGV4dD48cGF0aCBkPSJNNjM4LjUsODMyLjYwMTYgQzYzOC41LDgyMi42MDE2IDY1Ni41LDgyMi42MDE2IDY1Ni41LDgyMi42MDE2IEM2NTYuNSw4MjIuNjAxNiA2NzQuNSw4MjIuNjAxNiA2NzQuNSw4MzIuNjAxNiBMNjc0LjUsODU4LjYwMTYgQzY3NC41LDg2OC42MDE2IDY1Ni41LDg2OC42MDE2IDY1Ni41LDg2OC42MDE2IEM2NTYuNSw4NjguNjAxNiA2MzguNSw4NjguNjAxNiA2MzguNSw4NTguNjAxNiBMNjM4LjUsODMyLjYwMTYgIiBmaWxsPSIjRkVGRUNFIiBmaWx0ZXI9InVybCgjZjh2eHV6Y3h2dHV1NSkiIHN0eWxlPSJzdHJva2U6ICMwMDAwMDA7IHN0cm9rZS13aWR0aDogMS41OyIvPjxwYXRoIGQ9Ik02MzguNSw4MzIuNjAxNiBDNjM4LjUsODQyLjYwMTYgNjU2LjUsODQyLjYwMTYgNjU2LjUsODQyLjYwMTYgQzY1Ni41LDg0Mi42MDE2IDY3NC41LDg0Mi42MDE2IDY3NC41LDgzMi42MDE2ICIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTogIzAwMDAwMDsgc3Ryb2tlLXdpZHRoOiAxLjU7Ii8+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSIzNjcuNSwxMjcuMzgyOCwzNzcuNSwxMzEuMzgyOCwzNjcuNSwxMzUuMzgyOCwzNzEuNSwxMzEuMzgyOCIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IHN0cm9rZS1kYXNoYXJyYXk6IDIuMCwyLjA7IiB4MT0iNjQuNSIgeDI9IjM3My41IiB5MT0iMTMxLjM4MjgiIHkyPSIxMzEuMzgyOCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI1OCIgeD0iNzEuNSIgeT0iMTI2LjMxNjkiPmgzLmluaXQoKTwvdGV4dD48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjY0NC41LDE1Ni41MTU2LDY1NC41LDE2MC41MTU2LDY0NC41LDE2NC41MTU2LDY0OC41LDE2MC41MTU2IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiIHgxPSIzNzkuNSIgeDI9IjY1MC41IiB5MT0iMTYwLjUxNTYiIHkyPSIxNjAuNTE1NiIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXN0eWxlPSJpdGFsaWMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNTYiIHg9IjM4Ni41IiB5PSIxNTUuNDQ5NyI+aW5pdGlhbGl6ZTwvdGV4dD48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiIHgxPSI2NTYuNSIgeDI9IjY5OC41IiB5MT0iMTg5LjY0ODQiIHkyPSIxODkuNjQ4NCIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjY5OC41IiB4Mj0iNjk4LjUiIHkxPSIxODkuNjQ4NCIgeTI9IjIwMi42NDg0Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iNjU3LjUiIHgyPSI2OTguNSIgeTE9IjIwMi42NDg0IiB5Mj0iMjAyLjY0ODQiLz48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjY2Ny41LDE5OC42NDg0LDY1Ny41LDIwMi42NDg0LDY2Ny41LDIwNi42NDg0LDY2My41LDIwMi42NDg0IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC1zdHlsZT0iaXRhbGljIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjExMiIgeD0iNjYzLjUiIHk9IjE4NC41ODI1Ij5leGVjdXRlIG1vZHVsZXM8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI2NDQuNSwyMjcuNzgxMyw2NTQuNSwyMzEuNzgxMyw2NDQuNSwyMzUuNzgxMyw2NDguNSwyMzEuNzgxMyIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMzc5LjUiIHgyPSI2NTAuNSIgeTE9IjIzMS43ODEzIiB5Mj0iMjMxLjc4MTMiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI5MSIgeD0iMzg2LjUiIHk9IjIyNi43MTUzIj5kaXNwYXRjaCgkaW5pdCk8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI3NS41LDI1Ni45MTQxLDY1LjUsMjYwLjkxNDEsNzUuNSwyNjQuOTE0MSw3MS41LDI2MC45MTQxIiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiIHgxPSI2OS41IiB4Mj0iMzc4LjUiIHkxPSIyNjAuOTE0MSIgeTI9IjI2MC45MTQxIi8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNjQiIHg9IjgxLjUiIHk9IjI1NS44NDgxIj5wcmVTdGFydCgpPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iNDgxLjUsMjg2LjA0NjksNDkxLjUsMjkwLjA0NjksNDgxLjUsMjk0LjA0NjksNDg1LjUsMjkwLjA0NjkiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjM3OS41IiB4Mj0iNDg3LjUiIHkxPSIyOTAuMDQ2OSIgeTI9IjI5MC4wNDY5Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtc3R5bGU9Iml0YWxpYyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI1NiIgeD0iMzg2LjUiIHk9IjI4NC45ODEiPmluaXRpYWxpemU8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI0ODEuNSwzMTUuMTc5Nyw0OTEuNSwzMTkuMTc5Nyw0ODEuNSwzMjMuMTc5Nyw0ODUuNSwzMTkuMTc5NyIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMzc5LjUiIHgyPSI0ODcuNSIgeTE9IjMxOS4xNzk3IiB5Mj0iMzE5LjE3OTciLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI0MCIgeD0iMzg2LjUiIHk9IjMxNC4xMTM4Ij5zdGFydCgpPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iMjQ3LjUsMzQ0LjMxMjUsMjM3LjUsMzQ4LjMxMjUsMjQ3LjUsMzUyLjMxMjUsMjQzLjUsMzQ4LjMxMjUiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjI0MS41IiB4Mj0iNDkyLjUiIHkxPSIzNDguMzEyNSIgeTI9IjM0OC4zMTI1Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNTQiIHg9IjI1My41IiB5PSIzNDMuMjQ2NiI+cmVuZGVyKCk8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI2NDQuNSwzNzMuNDQ1Myw2NTQuNSwzNzcuNDQ1Myw2NDQuNSwzODEuNDQ1Myw2NDguNSwzNzcuNDQ1MyIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iNDkzLjUiIHgyPSI2NTAuNSIgeTE9IjM3Ny40NDUzIiB5Mj0iMzc3LjQ0NTMiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMTgiIHg9IjUwMC41IiB5PSIzNzIuMzc5NCI+ZGlzcGF0Y2goJHJlZHJhdyk8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI2NDQuNSw0MDIuNTc4MSw2NTQuNSw0MDYuNTc4MSw2NDQuNSw0MTAuNTc4MSw2NDguNSw0MDYuNTc4MSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iNDkzLjUiIHgyPSI2NTAuNSIgeTE9IjQwNi41NzgxIiB5Mj0iNDA2LjU3ODEiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMzkiIHg9IjUwMC41IiB5PSI0MDEuNTEyMiI+ZGlzcGF0Y2goJG5hdmlnYXRpb24pPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iNzUuNSw0MzEuNzEwOSw2NS41LDQzNS43MTA5LDc1LjUsNDM5LjcxMDksNzEuNSw0MzUuNzEwOSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iNjkuNSIgeDI9IjM3OC41IiB5MT0iNDM1LjcxMDkiIHkyPSI0MzUuNzEwOSIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjcwIiB4PSI4MS41IiB5PSI0MzAuNjQ1Ij5wb3N0U3RhcnQoKTwvdGV4dD48cGF0aCBkPSJNMTgyLjUsNDUwLjcxMDkgTDI4MC41LDQ1MC43MTA5IEwyODAuNSw0NTcuNzEwOSBMMjcwLjUsNDY3LjcxMDkgTDE4Mi41LDQ2Ny43MTA5IEwxODIuNSw0NTAuNzEwOSAiIGZpbGw9IiNFRUVFRUUiIHN0eWxlPSJzdHJva2U6ICMwMDAwMDA7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iMTA0LjUzMTMiIHN0eWxlPSJzdHJva2U6ICMwMDAwMDA7IHN0cm9rZS13aWR0aDogMi4wOyIgd2lkdGg9IjUwNi41IiB4PSIxODIuNSIgeT0iNDUwLjcxMDkiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNTMiIHg9IjE5Ny41IiB5PSI0NjMuNzc3OCI+cmVkcmF3PC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iMzY3LjUsNDg0Ljk3NjYsMzc3LjUsNDg4Ljk3NjYsMzY3LjUsNDkyLjk3NjYsMzcxLjUsNDg4Ljk3NjYiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjIzNi41IiB4Mj0iMzczLjUiIHkxPSI0ODguOTc2NiIgeTI9IjQ4OC45NzY2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9Ijg2IiB4PSIyNDMuNSIgeT0iNDgzLjkxMDYiPmgzLnJlZHJhdygpPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iMjQ3LjUsNTE0LjEwOTQsMjM3LjUsNTE4LjEwOTQsMjQ3LjUsNTIyLjEwOTQsMjQzLjUsNTE4LjEwOTQiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjI0MS41IiB4Mj0iMzc4LjUiIHkxPSI1MTguMTA5NCIgeTI9IjUxOC4xMDk0Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iNTYiIHg9IjI1My41IiB5PSI1MTMuMDQzNSI+cmVkcmF3KCk8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI2NDQuNSw1NDMuMjQyMiw2NTQuNSw1NDcuMjQyMiw2NDQuNSw1NTEuMjQyMiw2NDguNSw1NDcuMjQyMiIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMzc5LjUiIHgyPSI2NTAuNSIgeTE9IjU0Ny4yNDIyIiB5Mj0iNTQ3LjI0MjIiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSIxMTgiIHg9IjM4Ni41IiB5PSI1NDIuMTc2MyI+ZGlzcGF0Y2goJHJlZHJhdyk8L3RleHQ+PHBhdGggZD0iTTEzLDU2OS4yNDIyIEwxMzcsNTY5LjI0MjIgTDEzNyw1NzYuMjQyMiBMMTI3LDU4Ni4yNDIyIEwxMyw1ODYuMjQyMiBMMTMsNTY5LjI0MjIgIiBmaWxsPSIjRUVFRUVFIiBzdHlsZT0ic3Ryb2tlOiAjMDAwMDAwOyBzdHJva2Utd2lkdGg6IDEuMDsiLz48cmVjdCBmaWxsPSJub25lIiBoZWlnaHQ9IjIyMS4wNjI1IiBzdHlsZT0ic3Ryb2tlOiAjMDAwMDAwOyBzdHJva2Utd2lkdGg6IDIuMDsiIHdpZHRoPSI2NzYiIHg9IjEzIiB5PSI1NjkuMjQyMiIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI3OSIgeD0iMjgiIHk9IjU4Mi4zMDkxIj5uYXZpZ2F0aW9uPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iMzY3LjUsNjAzLjUwNzgsMzc3LjUsNjA3LjUwNzgsMzY3LjUsNjExLjUwNzgsMzcxLjUsNjA3LjUwNzgiIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjIzNi41IiB4Mj0iMzczLjUiIHkxPSI2MDcuNTA3OCIgeTI9IjYwNy41MDc4Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjExOSIgeD0iMjQzLjUiIHk9IjYwMi40NDE5Ij5oMy5uYXZpZ2F0ZVRvKCk8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSI0ODEuNSw2MzIuNjQwNiw0OTEuNSw2MzYuNjQwNiw0ODEuNSw2NDAuNjQwNiw0ODUuNSw2MzYuNjQwNiIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMzc5LjUiIHgyPSI0ODcuNSIgeTE9IjYzNi42NDA2IiB5Mj0iNjM2LjY0MDYiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI5MCIgeD0iMzg2LjUiIHk9IjYzMS41NzQ3Ij5wcm9jZXNzUGF0aCgpPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iNzUuNSw2NjEuNzczNCw2NS41LDY2NS43NzM0LDc1LjUsNjY5Ljc3MzQsNzEuNSw2NjUuNzczNCIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iNjkuNSIgeDI9IjQ5Mi41IiB5MT0iNjY1Ljc3MzQiIHkyPSI2NjUuNzczNCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXN0eWxlPSJpdGFsaWMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTQ4IiB4PSI4MS41IiB5PSI2NjAuNzA3NSI+cmVtb3ZlIGFsbCBET00gbm9kZXM8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSIyMjQuNSw2OTAuOTA2MywyMzQuNSw2OTQuOTA2MywyMjQuNSw2OTguOTA2MywyMjguNSw2OTQuOTA2MyIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iNjQuNSIgeDI9IjIzMC41IiB5MT0iNjk0LjkwNjMiIHkyPSI2OTQuOTA2MyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXN0eWxlPSJpdGFsaWMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTQ4IiB4PSI3MS41IiB5PSI2ODkuODQwMyI+cmVtb3ZlIGFsbCBET00gbm9kZXM8L3RleHQ+PHBvbHlnb24gZmlsbD0iI0E4MDAzNiIgcG9pbnRzPSIyNDcuNSw3MjAuMDM5MSwyMzcuNSw3MjQuMDM5MSwyNDcuNSw3MjguMDM5MSwyNDMuNSw3MjQuMDM5MSIgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTogI0E4MDAzNjsgc3Ryb2tlLXdpZHRoOiAxLjA7IiB4MT0iMjQxLjUiIHgyPSI0OTIuNSIgeTE9IjcyNC4wMzkxIiB5Mj0iNzI0LjAzOTEiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nQW5kR2x5cGhzIiB0ZXh0TGVuZ3RoPSI1NCIgeD0iMjUzLjUiIHk9IjcxOC45NzMxIj5yZW5kZXIoKTwvdGV4dD48cG9seWdvbiBmaWxsPSIjQTgwMDM2IiBwb2ludHM9IjY0NC41LDc0OS4xNzE5LDY1NC41LDc1My4xNzE5LDY0NC41LDc1Ny4xNzE5LDY0OC41LDc1My4xNzE5IiBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiAjQTgwMDM2OyBzdHJva2Utd2lkdGg6IDEuMDsiIHgxPSI0OTMuNSIgeDI9IjY1MC41IiB5MT0iNzUzLjE3MTkiIHkyPSI3NTMuMTcxOSIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmdBbmRHbHlwaHMiIHRleHRMZW5ndGg9IjExOCIgeD0iNTAwLjUiIHk9Ijc0OC4xMDYiPmRpc3BhdGNoKCRyZWRyYXcpPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiNBODAwMzYiIHBvaW50cz0iNjQ0LjUsNzc4LjMwNDcsNjU0LjUsNzgyLjMwNDcsNjQ0LjUsNzg2LjMwNDcsNjQ4LjUsNzgyLjMwNDciIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6ICNBODAwMzY7IHN0cm9rZS13aWR0aDogMS4wOyIgeDE9IjQ5My41IiB4Mj0iNjUwLjUiIHkxPSI3ODIuMzA0NyIgeTI9Ijc4Mi4zMDQ3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZ0FuZEdseXBocyIgdGV4dExlbmd0aD0iMTM5IiB4PSI1MDAuNSIgeT0iNzc3LjIzODgiPmRpc3BhdGNoKCRuYXZpZ2F0aW9uKTwvdGV4dD48IS0tTUQ1PVszNTJkNDMxYjJlY2QyZjllZTQ4YTdkNmVmYzg5MDQzNl0KQHN0YXJ0dW1sDQp0aXRsZSBIMyBTZXF1ZW5jZSBEaWFncmFtDQoNCmVudGl0eSBBcHBsaWNhdGlvbg0KY29udHJvbCBDb21wb25lbnQNCmVudGl0eSBIMw0KZW50aXR5IFJvdXRlcg0KZGF0YWJhc2UgU3RvcmUNCg0KQXBwbGljYXRpb24gLSAtPiBIMyA6IDxiPmgzLmluaXQoKTwvYj4NCkgzIC0+IFN0b3JlIDogLy9pbml0aWFsaXplLy8NClN0b3JlIC0+IFN0b3JlIDogLy9leGVjdXRlIG1vZHVsZXMvLw0KSDMgLT4gU3RvcmUgOiBkaXNwYXRjaCgkaW5pdCkNCkgzIC0+IEFwcGxpY2F0aW9uIDogcHJlU3RhcnQoKQ0KSDMgLT4gUm91dGVyIDogLy9pbml0aWFsaXplLy8NCkgzIC0+IFJvdXRlciA6IHN0YXJ0KCkNClJvdXRlciAtPiBDb21wb25lbnQgOiByZW5kZXIoKQ0KUm91dGVyIC0+IFN0b3JlOiBkaXNwYXRjaCgkcmVkcmF3KQ0KUm91dGVyIC0+IFN0b3JlOiBkaXNwYXRjaCgkbmF2aWdhdGlvbikNCkgzIC0+IEFwcGxpY2F0aW9uIDogcG9zdFN0YXJ0KCkNCg0KZ3JvdXAgcmVkcmF3DQogICAgQ29tcG9uZW50IC0+IEgzIDogPGI+aDMucmVkcmF3KCk8L2I+DQogICAgSDMgLT4gQ29tcG9uZW50IDogcmVkcmF3KCkNCiAgICBIMyAtPiBTdG9yZTogZGlzcGF0Y2goJHJlZHJhdykNCmVuZA0KDQpncm91cCBuYXZpZ2F0aW9uDQogICAgQ29tcG9uZW50IC0+IEgzIDogPGI+aDMubmF2aWdhdGVUbygpPC9iPg0KICAgIEgzIC0+IFJvdXRlciA6IHByb2Nlc3NQYXRoKCkNCiAgICBSb3V0ZXIgLT4gQXBwbGljYXRpb24gOiAvL3JlbW92ZSBhbGwgRE9NIG5vZGVzLy8NCiAgICBBcHBsaWNhdGlvbiAtPiBDb21wb25lbnQgOiAvL3JlbW92ZSBhbGwgRE9NIG5vZGVzLy8NCiAgICBSb3V0ZXIgLT4gQ29tcG9uZW50IDogcmVuZGVyKCkNCiAgICBSb3V0ZXIgLT4gU3RvcmU6IGRpc3BhdGNoKCRyZWRyYXcpDQogICAgUm91dGVyIC0+IFN0b3JlOiBkaXNwYXRjaCgkbmF2aWdhdGlvbikNCmVuZA0KQGVuZHVtbA0KClBsYW50VU1MIHZlcnNpb24gMS4yMDIwLjA4YmV0YTEoVW5rbm93biBjb21waWxlIHRpbWUpCihHUEwgc291cmNlIGRpc3RyaWJ1dGlvbikKSmF2YSBSdW50aW1lOiBKYXZhKFRNKSBTRSBSdW50aW1lIEVudmlyb25tZW50CkpWTTogSmF2YSBIb3RTcG90KFRNKSA2NC1CaXQgU2VydmVyIFZNCkphdmEgVmVyc2lvbjogMTQuMC4xKzcKT3BlcmF0aW5nIFN5c3RlbTogTGludXgKRGVmYXVsdCBFbmNvZGluZzogVVRGLTgKTGFuZ3VhZ2U6IGVuCkNvdW50cnk6IFVTCi0tPjwvZz48L3N2Zz4=" alt="Sequence Diagram" /></p> + +<p>When the <code>h3.init()</code> method is called at application level, the following operations are performed in sequence:</p> + +<ol> +<li>The <em>Store</em> is created and initialized.</li> +<li>Any <em>Module</em> specified when calling <code>h3.init()</code> is executed.</li> +<li>The <strong>$init</strong> message is dispatched.</li> +<li>The <em>preStart</em> function (if specified when calling <code>h3.init()</code>) is executed.</li> +<li>The <em>Router</em> is initialized and started.</li> +<li>All <em>Components</em> matching the current route are rendered for the first time.</li> +<li>The <strong>$redraw</strong> and <strong>$navigation</strong> messages are dispatched.</li> +</ol> + + +<p>Then, whenever the <code>h3.redraw()</code> method is called (typically within a component):</p> + +<ol> +<li>The whole application is redrawn, i.e. every <em>Component</em> currently rendered on the page is redrawn.</li> +<li>The <strong>$redraw</strong> message is dispatched.</li> +</ol> + + +<p>Similarly, whenever the <code>h3.navigateTo()</code> method is called (typically within a component), or the URL fragment changes:</p> + +<ol> +<li>The <em>Router</em> processes the new path and determine which component to render based on the routing configuration.</li> +<li>All DOM nodes within the scope of the routing are removed, all components are removed.</li> +<li>The <em>Component</em> matching the new route is rendered.</li> +<li>The <strong>$redraw</strong> and <strong>$navigation</strong> messages are dispatched.</li> +</ol> + + +<p>And that&rsquo;s it. The whole idea is to make the system extremely <em>simple</em> and <em>predictable</em> &mdash; which means everything should be very easy to debug, too.</p> + +<a name="Tutorial"></a> +<h2>Tutorial<a href="#document-top" title="Go to top"></a></h2> <p>As a (meta) explanation of how to use H3, let&rsquo;s have a look at how the <a href="https://h3.js.org">H3 web site</a> itself was created.</p>

@@ -7555,7 +7597,7 @@ <pre><code class="js">const labels = {

overview: "Overview", "quick-start": "Quick Start", "key-concepts": "Key Concepts", - usage: "Usage", + tutorial: "Tutorial", api: "API", about: "About", };

@@ -7584,83 +7626,100 @@ <li>Triggers a redraw of the application</li>

</ol> -<p>Then it&rsquo;s time to create a simple <code>Page</code> component that actually renders the markup of the page:</p> +<p>We are gonna use our <code>fetchPage</code> function inside the main component of our app, <code>Page</code>:</p> <pre><code class="js">const Page = () =&gt; { const id = h3.route.path.slice(1); const ids = Object.keys(labels); const md = ids.includes(id) ? `md/${id}.md` : `md/overview.md`; fetchPage(pages, id, md); - const menu = ids.map((p) =&gt; - h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) - ); - let content = pages[id] - ? h3("div.content", { $html: pages[id] }) - : h3("div.spinner-container", h3("span.spinner")); return h3("div.page", [ - h3("header.row.sticky", [ - h3( - "a.logo.col-sm-1", - { href: "#/" }, - [h3("img", { alt: "H3", src: "images/h3.svg" })] - ), - h3("div.version.col-sm.col-md", [ - h3("div.version-number", "v0.1.0"), - h3("div.version-label", "“Audacious Andorian“"), - ]), - h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), - ]), + Header, h3("div.row", [ h3("input#drawer-control.drawer", { type: "checkbox" }), - h3("nav#navigation.col-md-3", [ - h3("label.drawer-close", { for: "drawer-control" }), - ...menu, - ]), - h3("main.col-sm-12.col-md-9", [ - h3("div.card.fluid", h3("div.section", content)), - ]), - h3( - "footer", - h3("div", [ - "© 2020 Fabio Cevasco · ", - h3( - "a", - { - href: "https://h3.js.org/H3_DeveloperGuide.htm", - target: "_blank", - }, - "Download the Guide" - ), - ]) - ), + Navigation(id, ids), + Content(pages[id]), + Footer, ]), ]); }; </code></pre> -<p>This component is essentially able to render any Markdown page based on the current route (URL fragment).</p> +<p>The main responsibility of this component is to fetch the Markdown content and render the whole page, but note how the rendering different portions of the page are delegated to different components: <code>Header</code>, <code>Navigation</code>, <code>Content</code>, and <code>Footer</code>.</p> -<p>Suppose for example that the <code>#/overview</code> page is loaded. The <code>h3.route.path</code> in this case is going to be set to <code>/overview</code>, which in turns corresponds to an ID of a well-known page (<code>overview</code>).</p> +<p>The <code>Header</code> and <code>Footer</code> component are very simple, as their only job is to render static content. Both component simply return a tree of VNodes:</p> + +<pre><code class="js">const Header = () =&gt; { + return h3("header.row.sticky", [ + h3("a.logo.col-sm-1", { href: "#/" }, [ + h3("img", { alt: "H3", src: "images/h3.svg" }), + ]), + h3("div.version.col-sm.col-md", [ + h3("div.version-number", "v0.1.0"), + h3("div.version-label", "“Audacious Andorian“"), + ]), + h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), + ]); +}; + +const Footer = () =&gt; { + return h3( + "footer", + h3("div", [ + "© 2020 Fabio Cevasco · ", + h3( + "a", + { + href: "H3_DeveloperGuide.htm", + target: "_blank", + }, + "Download the Guide" + ), + ]) + ); +}; +</code></pre> + +<p>The <code>Navigation</code> component is more interesting, as it takes two parameters:</p> -<p>In a similar way, other well-known pages can easily be mapped to IDs, but it is also important to handle <em>unknown</em> pages (technically I could even pass an URL to a different site containing a malicious markdown page and have it rendered!), and if a page passed in the URL fragment is not present in the <code>labels</code> Object, the Overview page will be rendered instead.</p> +<ul> +<li>The ID of the current page</li> +<li>The list of page IDs</li> +</ul> -<p>This feature is also handy to automatically load the Overview when no fragment is specified.</p> -<p>Note then how the web site menu is created based on the <code>labels</code> object:</p> +<p>&hellip;and it uses this information to create the site navigation menu dynamically:</p> -<pre><code class="js">const menu = ids.map((p) =&gt; h3(`a${p === id ? '.active' : ''}`, { href: `#/${p}` }, labels[p])); +<pre><code class="js">const Navigation = (id, ids) =&gt; { + const menu = ids.map((p) =&gt; + h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) + ); + return h3("nav#navigation.col-md-3", [ + h3("label.drawer-close", { for: "drawer-control" }), + ...menu, + ]); +}; </code></pre> -<p>Also, the <code>active</code> class will be applied for the currently-active link.</p> +<p>Finally, the <code>Content</code> component optionally takes a string containing the HTML of the page content to render. If no content is provided, it will display a loading spinner, otherwise it will render the content by using the special <code>$html</code> attribute that can be used to essentially set the <code>innerHTML</code> of an element:</p> -<p>Finally, the last noteworthy thing of this code is how the HTML code of each page is rendered:</p> - -<pre><code class="js">let content = pages[id] - ? h3("div.content", { $html: pages[id] }) +<pre><code class="js">const Content = (html) =&gt; { + const content = html + ? h3("div.content", { $html: html }) : h3("div.spinner-container", h3("span.spinner")); + return h3("main.col-sm-12.col-md-9", [ + h3("div.card.fluid", h3("div.section", content)), + ]); +}; </code></pre> -<p>If the content has been loaded, the page content will be added as raw HTML to the <code>div.content</code> element (no sanitization needed as we are only going to ever render well-known Markdown files), otherwise a spinner will be displayed (until the application is re-rendered anyway).</p> +<p>Now, the key here is that we are only ever going to render &ldquo;known&rdquo; pages that are listed in the <code>labels</code> object.</p> + +<p>Suppose for example that the <code>#/overview</code> page is loaded. The <code>h3.route.path</code> in this case is going to be set to <code>/overview</code>, which in turns corresponds to an ID of a well-known page (<code>overview</code>).</p> + +<p>In a similar way, other well-known pages can easily be mapped to IDs, but it is also important to handle <em>unknown</em> pages (technically I could even pass an URL to a different site containing a malicious markdown page and have it rendered!), and if a page passed in the URL fragment is not present in the <code>labels</code> Object, the Overview page will be rendered instead.</p> + +<p>This feature is also handy to automatically load the Overview when no fragment is specified.</p> <a name="Initialization-and-post-redraw-operations"></a> <h3>Initialization and post-redraw operations<a href="#document-top" title="Go to top"></a></h3>

@@ -7940,9 +7999,20 @@ <li><a href="https://prismjs.com/">Prism.js</a></li>

<li><a href="https://minicss.org/">mini.css</a></li> </ul> + +<a name="Special-Thanks"></a> +<h3>Special Thanks<a href="#document-top" title="Go to top"></a></h3> + +<p>Special thanks to the following individuals, that made H3 possible:</p> + +<ul> +<li><strong>Leo Horie</strong>, author of the awesome <a href="https://mithril.js.org/">Mithril</a> framework that inspired me to write the H3 microframework in a moment of need.</li> +<li><strong>Andrey Sitnik</strong>, author of the beatiful <a href="https://evilmartians.com/chronicles/storeon-redux-in-173-bytes">Storeon</a> state management library, that is used (with minor modification) as the H3 store.</li> +</ul> + </div> <div id="footer"> - <p><span class="copy"></span> Fabio Cevasco &ndash; April 20, 2020</p> + <p><span class="copy"></span> Fabio Cevasco &ndash; April 21, 2020</p> <p><span>Powered by</span> <a href="https://h3rald.com/hastyscribe"><span class="hastyscribe"></span></a></p> </div> </div>
M docs/H3_DeveloperGuide.mddocs/H3_DeveloperGuide.md

@@ -14,7 +14,7 @@ {@ md/quick-start.md || 0 @}

{@ md/key-concepts.md || 0 @} -{@ md/usage.md || 0 @} +{@ md/tutorial.md || 0 @} {@ md/api.md || 0 @}
M docs/example/assets/js/h3.jsdocs/example/assets/js/h3.js

@@ -1,3 +1,11 @@

+ +/** + * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com> + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ const equal = (obj1, obj2) => { if ( (obj1 === null && obj2 === null) ||

@@ -464,8 +472,12 @@ }

} } -// Simple store based on Storeon -// https://github.com/storeon/storeon/blob/master/index.js +/** + * The code of the following class is heavily based on Storeon + * Modified according to the terms of the MIT License + * <https://github.com/storeon/storeon/blob/master/LICENSE> + * Copyright 2019 Andrey Sitnik <andrey@sitnik.ru> + */ class Store { constructor() { this.events = {};
A docs/images/h3.sequence.svg

@@ -0,0 +1,49 @@

+<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="880px" preserveAspectRatio="none" style="width:787px;height:880px;" version="1.1" viewBox="0 0 787 880" width="787px" zoomAndPan="magnify"><defs><filter height="300%" id="f8vxuzcxvtuu5" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><text fill="#000000" font-family="sans-serif" font-size="18" lengthAdjust="spacingAndGlyphs" textLength="208" x="288.75" y="26.708">H3 Sequence Diagram</text><rect fill="#FFFFFF" filter="url(#f8vxuzcxvtuu5)" height="104.5313" style="stroke: #000000; stroke-width: 2.0;" width="506.5" x="182.5" y="450.7109"/><rect fill="#FFFFFF" filter="url(#f8vxuzcxvtuu5)" height="221.0625" style="stroke: #000000; stroke-width: 2.0;" width="676" x="13" y="569.2422"/><line style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 5.0,5.0;" x1="64" x2="64" y1="100.25" y2="807.3047"/><line style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 5.0,5.0;" x1="236.5" x2="236.5" y1="100.25" y2="807.3047"/><line style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 5.0,5.0;" x1="379.5" x2="379.5" y1="100.25" y2="807.3047"/><line style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 5.0,5.0;" x1="493" x2="493" y1="100.25" y2="807.3047"/><line style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 5.0,5.0;" x1="656" x2="656" y1="100.25" y2="807.3047"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="77" x="23" y="96.9482">Application</text><ellipse cx="64.5" cy="67.9531" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><line style="stroke: #A80036; stroke-width: 2.0;" x1="52.5" x2="76.5" y1="81.9531" y2="81.9531"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="77" x="23" y="819.2998">Application</text><ellipse cx="64.5" cy="838.6016" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><line style="stroke: #A80036; stroke-width: 2.0;" x1="52.5" x2="76.5" y1="852.6016" y2="852.6016"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="192.5" y="96.9482">Component</text><ellipse cx="236.5" cy="67.9531" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><polygon fill="#A80036" points="232.5,55.9531,238.5,50.9531,236.5,55.9531,238.5,60.9531,232.5,55.9531" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="192.5" y="819.2998">Component</text><ellipse cx="236.5" cy="838.6016" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><polygon fill="#A80036" points="232.5,826.6016,238.5,821.6016,236.5,826.6016,238.5,831.6016,232.5,826.6016" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="18" x="367.5" y="96.9482">H3</text><ellipse cx="379.5" cy="67.9531" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><line style="stroke: #A80036; stroke-width: 2.0;" x1="367.5" x2="391.5" y1="81.9531" y2="81.9531"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="18" x="367.5" y="819.2998">H3</text><ellipse cx="379.5" cy="838.6016" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><line style="stroke: #A80036; stroke-width: 2.0;" x1="367.5" x2="391.5" y1="852.6016" y2="852.6016"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="47" x="467" y="96.9482">Router</text><ellipse cx="493.5" cy="67.9531" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><line style="stroke: #A80036; stroke-width: 2.0;" x1="481.5" x2="505.5" y1="81.9531" y2="81.9531"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="47" x="467" y="819.2998">Router</text><ellipse cx="493.5" cy="838.6016" fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" rx="12" ry="12" style="stroke: #A80036; stroke-width: 2.0;"/><line style="stroke: #A80036; stroke-width: 2.0;" x1="481.5" x2="505.5" y1="852.6016" y2="852.6016"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="39" x="634" y="96.9482">Store</text><path d="M638.5,47.9531 C638.5,37.9531 656.5,37.9531 656.5,37.9531 C656.5,37.9531 674.5,37.9531 674.5,47.9531 L674.5,73.9531 C674.5,83.9531 656.5,83.9531 656.5,83.9531 C656.5,83.9531 638.5,83.9531 638.5,73.9531 L638.5,47.9531 " fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" style="stroke: #000000; stroke-width: 1.5;"/><path d="M638.5,47.9531 C638.5,57.9531 656.5,57.9531 656.5,57.9531 C656.5,57.9531 674.5,57.9531 674.5,47.9531 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="39" x="634" y="819.2998">Store</text><path d="M638.5,832.6016 C638.5,822.6016 656.5,822.6016 656.5,822.6016 C656.5,822.6016 674.5,822.6016 674.5,832.6016 L674.5,858.6016 C674.5,868.6016 656.5,868.6016 656.5,868.6016 C656.5,868.6016 638.5,868.6016 638.5,858.6016 L638.5,832.6016 " fill="#FEFECE" filter="url(#f8vxuzcxvtuu5)" style="stroke: #000000; stroke-width: 1.5;"/><path d="M638.5,832.6016 C638.5,842.6016 656.5,842.6016 656.5,842.6016 C656.5,842.6016 674.5,842.6016 674.5,832.6016 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><polygon fill="#A80036" points="367.5,127.3828,377.5,131.3828,367.5,135.3828,371.5,131.3828" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 2.0,2.0;" x1="64.5" x2="373.5" y1="131.3828" y2="131.3828"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="58" x="71.5" y="126.3169">h3.init()</text><polygon fill="#A80036" points="644.5,156.5156,654.5,160.5156,644.5,164.5156,648.5,160.5156" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="379.5" x2="650.5" y1="160.5156" y2="160.5156"/><text fill="#000000" font-family="sans-serif" font-size="13" font-style="italic" lengthAdjust="spacingAndGlyphs" textLength="56" x="386.5" y="155.4497">initialize</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="656.5" x2="698.5" y1="189.6484" y2="189.6484"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="698.5" x2="698.5" y1="189.6484" y2="202.6484"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="657.5" x2="698.5" y1="202.6484" y2="202.6484"/><polygon fill="#A80036" points="667.5,198.6484,657.5,202.6484,667.5,206.6484,663.5,202.6484" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" font-style="italic" lengthAdjust="spacingAndGlyphs" textLength="112" x="663.5" y="184.5825">execute modules</text><polygon fill="#A80036" points="644.5,227.7813,654.5,231.7813,644.5,235.7813,648.5,231.7813" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="379.5" x2="650.5" y1="231.7813" y2="231.7813"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="91" x="386.5" y="226.7153">dispatch($init)</text><polygon fill="#A80036" points="75.5,256.9141,65.5,260.9141,75.5,264.9141,71.5,260.9141" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="69.5" x2="378.5" y1="260.9141" y2="260.9141"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="64" x="81.5" y="255.8481">preStart()</text><polygon fill="#A80036" points="481.5,286.0469,491.5,290.0469,481.5,294.0469,485.5,290.0469" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="379.5" x2="487.5" y1="290.0469" y2="290.0469"/><text fill="#000000" font-family="sans-serif" font-size="13" font-style="italic" lengthAdjust="spacingAndGlyphs" textLength="56" x="386.5" y="284.981">initialize</text><polygon fill="#A80036" points="481.5,315.1797,491.5,319.1797,481.5,323.1797,485.5,319.1797" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="379.5" x2="487.5" y1="319.1797" y2="319.1797"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="40" x="386.5" y="314.1138">start()</text><polygon fill="#A80036" points="247.5,344.3125,237.5,348.3125,247.5,352.3125,243.5,348.3125" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="241.5" x2="492.5" y1="348.3125" y2="348.3125"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="54" x="253.5" y="343.2466">render()</text><polygon fill="#A80036" points="644.5,373.4453,654.5,377.4453,644.5,381.4453,648.5,377.4453" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="493.5" x2="650.5" y1="377.4453" y2="377.4453"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="118" x="500.5" y="372.3794">dispatch($redraw)</text><polygon fill="#A80036" points="644.5,402.5781,654.5,406.5781,644.5,410.5781,648.5,406.5781" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="493.5" x2="650.5" y1="406.5781" y2="406.5781"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="139" x="500.5" y="401.5122">dispatch($navigation)</text><polygon fill="#A80036" points="75.5,431.7109,65.5,435.7109,75.5,439.7109,71.5,435.7109" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="69.5" x2="378.5" y1="435.7109" y2="435.7109"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="70" x="81.5" y="430.645">postStart()</text><path d="M182.5,450.7109 L280.5,450.7109 L280.5,457.7109 L270.5,467.7109 L182.5,467.7109 L182.5,450.7109 " fill="#EEEEEE" style="stroke: #000000; stroke-width: 1.0;"/><rect fill="none" height="104.5313" style="stroke: #000000; stroke-width: 2.0;" width="506.5" x="182.5" y="450.7109"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="53" x="197.5" y="463.7778">redraw</text><polygon fill="#A80036" points="367.5,484.9766,377.5,488.9766,367.5,492.9766,371.5,488.9766" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="236.5" x2="373.5" y1="488.9766" y2="488.9766"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="86" x="243.5" y="483.9106">h3.redraw()</text><polygon fill="#A80036" points="247.5,514.1094,237.5,518.1094,247.5,522.1094,243.5,518.1094" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="241.5" x2="378.5" y1="518.1094" y2="518.1094"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="56" x="253.5" y="513.0435">redraw()</text><polygon fill="#A80036" points="644.5,543.2422,654.5,547.2422,644.5,551.2422,648.5,547.2422" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="379.5" x2="650.5" y1="547.2422" y2="547.2422"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="118" x="386.5" y="542.1763">dispatch($redraw)</text><path d="M13,569.2422 L137,569.2422 L137,576.2422 L127,586.2422 L13,586.2422 L13,569.2422 " fill="#EEEEEE" style="stroke: #000000; stroke-width: 1.0;"/><rect fill="none" height="221.0625" style="stroke: #000000; stroke-width: 2.0;" width="676" x="13" y="569.2422"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="79" x="28" y="582.3091">navigation</text><polygon fill="#A80036" points="367.5,603.5078,377.5,607.5078,367.5,611.5078,371.5,607.5078" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="236.5" x2="373.5" y1="607.5078" y2="607.5078"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="119" x="243.5" y="602.4419">h3.navigateTo()</text><polygon fill="#A80036" points="481.5,632.6406,491.5,636.6406,481.5,640.6406,485.5,636.6406" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="379.5" x2="487.5" y1="636.6406" y2="636.6406"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="90" x="386.5" y="631.5747">processPath()</text><polygon fill="#A80036" points="75.5,661.7734,65.5,665.7734,75.5,669.7734,71.5,665.7734" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="69.5" x2="492.5" y1="665.7734" y2="665.7734"/><text fill="#000000" font-family="sans-serif" font-size="13" font-style="italic" lengthAdjust="spacingAndGlyphs" textLength="148" x="81.5" y="660.7075">remove all DOM nodes</text><polygon fill="#A80036" points="224.5,690.9063,234.5,694.9063,224.5,698.9063,228.5,694.9063" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="64.5" x2="230.5" y1="694.9063" y2="694.9063"/><text fill="#000000" font-family="sans-serif" font-size="13" font-style="italic" lengthAdjust="spacingAndGlyphs" textLength="148" x="71.5" y="689.8403">remove all DOM nodes</text><polygon fill="#A80036" points="247.5,720.0391,237.5,724.0391,247.5,728.0391,243.5,724.0391" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="241.5" x2="492.5" y1="724.0391" y2="724.0391"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="54" x="253.5" y="718.9731">render()</text><polygon fill="#A80036" points="644.5,749.1719,654.5,753.1719,644.5,757.1719,648.5,753.1719" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="493.5" x2="650.5" y1="753.1719" y2="753.1719"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="118" x="500.5" y="748.106">dispatch($redraw)</text><polygon fill="#A80036" points="644.5,778.3047,654.5,782.3047,644.5,786.3047,648.5,782.3047" style="stroke: #A80036; stroke-width: 1.0;"/><line style="stroke: #A80036; stroke-width: 1.0;" x1="493.5" x2="650.5" y1="782.3047" y2="782.3047"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="139" x="500.5" y="777.2388">dispatch($navigation)</text><!--MD5=[352d431b2ecd2f9ee48a7d6efc890436] +@startuml +title H3 Sequence Diagram + +entity Application +control Component +entity H3 +entity Router +database Store + +Application - -> H3 : <b>h3.init()</b> +H3 -> Store : //initialize// +Store -> Store : //execute modules// +H3 -> Store : dispatch($init) +H3 -> Application : preStart() +H3 -> Router : //initialize// +H3 -> Router : start() +Router -> Component : render() +Router -> Store: dispatch($redraw) +Router -> Store: dispatch($navigation) +H3 -> Application : postStart() + +group redraw + Component -> H3 : <b>h3.redraw()</b> + H3 -> Component : redraw() + H3 -> Store: dispatch($redraw) +end + +group navigation + Component -> H3 : <b>h3.navigateTo()</b> + H3 -> Router : processPath() + Router -> Application : //remove all DOM nodes// + Application -> Component : //remove all DOM nodes// + Router -> Component : render() + Router -> Store: dispatch($redraw) + Router -> Store: dispatch($navigation) +end +@enduml + +PlantUML version 1.2020.08beta1(Unknown compile time) +(GPL source distribution) +Java Runtime: Java(TM) SE Runtime Environment +JVM: Java HotSpot(TM) 64-Bit Server VM +Java Version: 14.0.1+7 +Operating System: Linux +Default Encoding: UTF-8 +Language: en +Country: US +--></g></svg>
M docs/js/app.jsdocs/js/app.js

@@ -6,7 +6,7 @@ const labels = {

overview: "Overview", "quick-start": "Quick Start", "key-concepts": "Key Concepts", - usage: "Usage", + tutorial: "Tutorial", api: "API", about: "About", };

@@ -27,49 +27,63 @@ const id = h3.route.path.slice(1);

const ids = Object.keys(labels); const md = ids.includes(id) ? `md/${id}.md` : `md/overview.md`; fetchPage(pages, id, md); - const menu = ids.map((p) => - h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) - ); - let content = pages[id] - ? h3("div.content", { $html: pages[id] }) - : h3("div.spinner-container", h3("span.spinner")); return h3("div.page", [ - h3("header.row.sticky", [ - h3( - "a.logo.col-sm-1", - { href: "#/" }, - [h3("img", { alt: "H3", src: "images/h3.svg" })] - ), - h3("div.version.col-sm.col-md", [ - h3("div.version-number", "v0.1.0"), - h3("div.version-label", "“Audacious Andorian“"), - ]), - h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), - ]), + Header, h3("div.row", [ h3("input#drawer-control.drawer", { type: "checkbox" }), - h3("nav#navigation.col-md-3", [ - h3("label.drawer-close", { for: "drawer-control" }), - ...menu, - ]), - h3("main.col-sm-12.col-md-9", [ - h3("div.card.fluid", h3("div.section", content)), - ]), + Navigation(id, ids), + Content(pages[id]), + Footer, + ]), + ]); +}; + +const Header = () => { + return h3("header.row.sticky", [ + h3("a.logo.col-sm-1", { href: "#/" }, [ + h3("img", { alt: "H3", src: "images/h3.svg" }), + ]), + h3("div.version.col-sm.col-md", [ + h3("div.version-number", "v0.1.0"), + h3("div.version-label", "“Audacious Andorian“"), + ]), + h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), + ]); +}; + +const Footer = () => { + return h3( + "footer", + h3("div", [ + "© 2020 Fabio Cevasco · ", h3( - "footer", - h3("div", [ - "© 2020 Fabio Cevasco · ", - h3( - "a", - { - href: "https://h3.js.org/H3_DeveloperGuide.htm", - target: "_blank", - }, - "Download the Guide" - ), - ]) + "a", + { + href: "H3_DeveloperGuide.htm", + target: "_blank", + }, + "Download the Guide" ), - ]), + ]) + ); +}; + +const Navigation = (id, ids) => { + const menu = ids.map((p) => + h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) + ); + return h3("nav#navigation.col-md-3", [ + h3("label.drawer-close", { for: "drawer-control" }), + ...menu, + ]); +}; + +const Content = (html) => { + const content = html + ? h3("div.content", { $html: html }) + : h3("div.spinner-container", h3("span.spinner")); + return h3("main.col-sm-12.col-md-9", [ + h3("div.card.fluid", h3("div.section", content)), ]); };
M docs/js/h3.jsdocs/js/h3.js

@@ -1,3 +1,11 @@

+ +/** + * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com> + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ const equal = (obj1, obj2) => { if ( (obj1 === null && obj2 === null) ||

@@ -464,8 +472,12 @@ }

} } -// Simple store based on Storeon -// https://github.com/storeon/storeon/blob/master/index.js +/** + * The code of the following class is heavily based on Storeon + * Modified according to the terms of the MIT License + * <https://github.com/storeon/storeon/blob/master/LICENSE> + * Copyright 2019 Andrey Sitnik <andrey@sitnik.ru> + */ class Store { constructor() { this.events = {};
M docs/md/about.mddocs/md/about.md

@@ -32,4 +32,11 @@ The H3 web site is [built with H3 itself](https://github.com/h3rald/h3/blob/master/docs/js/app.js), plus the following third-party libraries:

* [marked.js](https://marked.js.org/#/README.md#README.md) * [Prism.js](https://prismjs.com/) -* [mini.css](https://minicss.org/)+* [mini.css](https://minicss.org/) + +### Special Thanks + +Special thanks to the following individuals, that made H3 possible: + +* **Leo Horie**, author of the awesome [Mithril](https://mithril.js.org/) framework that inspired me to write the H3 microframework in a moment of need. +* **Andrey Sitnik**, author of the beatiful [Storeon](https://evilmartians.com/chronicles/storeon-redux-in-173-bytes) state management library, that is used (with minor modification) as the H3 store.
M docs/md/key-concepts.mddocs/md/key-concepts.md

@@ -76,3 +76,33 @@

H3 comes with a very minimal but fully functional URL fragment router. You create your application routes when initializing your application, and you can navigate to them using ordinary `href` links or programmatically using the `h3.navigateTo` method. The current route is always accessible via the `h3.route` property. + +### Sequence Diagram + +The following sequence diagram summarizes how H3 works, from its initialization to the redraw and navigation phases. + +![Sequence Diagram](images/h3.sequence.svg) + +When the `h3.init()` method is called at application level, the following operations are performed in sequence: + +1. The *Store* is created and initialized. +2. Any *Module* specified when calling `h3.init()` is executed. +3. The **$init** message is dispatched. +4. The *preStart* function (if specified when calling `h3.init()`) is executed. +5. The *Router* is initialized and started. +6. All *Components* matching the current route are rendered for the first time. +7. The **$redraw** and **$navigation** messages are dispatched. + +Then, whenever the `h3.redraw()` method is called (typically within a component): + +1. The whole application is redrawn, i.e. every *Component* currently rendered on the page is redrawn. +2. The **$redraw** message is dispatched. + +Similarly, whenever the `h3.navigateTo()` method is called (typically within a component), or the URL fragment changes: + +1. The *Router* processes the new path and determine which component to render based on the routing configuration. +2. All DOM nodes within the scope of the routing are removed, all components are removed. +3. The *Component* matching the new route is rendered. +4. The **$redraw** and **$navigation** messages are dispatched. + +And that's it. The whole idea is to make the system extremely *simple* and *predictable* &mdash; which means everything should be very easy to debug, too.
M docs/md/usage.mddocs/md/tutorial.md

@@ -1,4 +1,4 @@

-## Usage +## Tutorial As a (meta) explanation of how to use H3, let's have a look at how the [H3 web site](https://h3.js.org) itself was created.

@@ -59,7 +59,7 @@ const labels = {

overview: "Overview", "quick-start": "Quick Start", "key-concepts": "Key Concepts", - usage: "Usage", + tutorial: "Tutorial", api: "API", about: "About", };

@@ -87,7 +87,7 @@ 1. fetches the content of the requested file (`md`))

2. renders the Markdown code into HTML using marked, and stores it in the `pages` object 3. Triggers a redraw of the application -Then it's time to create a simple `Page` component that actually renders the markup of the page: +We are gonna use our `fetchPage` function inside the main component of our app, `Page`: ```js const Page = () => {

@@ -95,78 +95,93 @@ const id = h3.route.path.slice(1);

const ids = Object.keys(labels); const md = ids.includes(id) ? `md/${id}.md` : `md/overview.md`; fetchPage(pages, id, md); - const menu = ids.map((p) => - h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) - ); - let content = pages[id] - ? h3("div.content", { $html: pages[id] }) - : h3("div.spinner-container", h3("span.spinner")); return h3("div.page", [ - h3("header.row.sticky", [ - h3( - "a.logo.col-sm-1", - { href: "#/" }, - [h3("img", { alt: "H3", src: "images/h3.svg" })] - ), - h3("div.version.col-sm.col-md", [ - h3("div.version-number", "v0.1.0"), - h3("div.version-label", "“Audacious Andorian“"), - ]), - h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), - ]), + Header, h3("div.row", [ h3("input#drawer-control.drawer", { type: "checkbox" }), - h3("nav#navigation.col-md-3", [ - h3("label.drawer-close", { for: "drawer-control" }), - ...menu, - ]), - h3("main.col-sm-12.col-md-9", [ - h3("div.card.fluid", h3("div.section", content)), - ]), - h3( - "footer", - h3("div", [ - "© 2020 Fabio Cevasco · ", - h3( - "a", - { - href: "https://h3.js.org/H3_DeveloperGuide.htm", - target: "_blank", - }, - "Download the Guide" - ), - ]) - ), + Navigation(id, ids), + Content(pages[id]), + Footer, ]), ]); }; ``` -This component is essentially able to render any Markdown page based on the current route (URL fragment). +The main responsibility of this component is to fetch the Markdown content and render the whole page, but note how the rendering different portions of the page are delegated to different components: `Header`, `Navigation`, `Content`, and `Footer`. + +The `Header` and `Footer` component are very simple, as their only job is to render static content. Both component simply return a tree of VNodes: + +```js +const Header = () => { + return h3("header.row.sticky", [ + h3("a.logo.col-sm-1", { href: "#/" }, [ + h3("img", { alt: "H3", src: "images/h3.svg" }), + ]), + h3("div.version.col-sm.col-md", [ + h3("div.version-number", "v0.1.0"), + h3("div.version-label", "“Audacious Andorian“"), + ]), + h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), + ]); +}; -Suppose for example that the `#/overview` page is loaded. The `h3.route.path` in this case is going to be set to `/overview`, which in turns corresponds to an ID of a well-known page (`overview`). +const Footer = () => { + return h3( + "footer", + h3("div", [ + "© 2020 Fabio Cevasco · ", + h3( + "a", + { + href: "H3_DeveloperGuide.htm", + target: "_blank", + }, + "Download the Guide" + ), + ]) + ); +}; +``` -In a similar way, other well-known pages can easily be mapped to IDs, but it is also important to handle _unknown_ pages (technically I could even pass an URL to a different site containing a malicious markdown page and have it rendered!), and if a page passed in the URL fragment is not present in the `labels` Object, the Overview page will be rendered instead. +The `Navigation` component is more interesting, as it takes two parameters: -This feature is also handy to automatically load the Overview when no fragment is specified. +* The ID of the current page +* The list of page IDs -Note then how the web site menu is created based on the `labels` object: +...and it uses this information to create the site navigation menu dynamically: ```js -const menu = ids.map((p) => h3(`a${p === id ? '.active' : ''}`, { href: `#/${p}` }, labels[p])); +const Navigation = (id, ids) => { + const menu = ids.map((p) => + h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) + ); + return h3("nav#navigation.col-md-3", [ + h3("label.drawer-close", { for: "drawer-control" }), + ...menu, + ]); +}; ``` -Also, the `active` class will be applied for the currently-active link. - -Finally, the last noteworthy thing of this code is how the HTML code of each page is rendered: +Finally, the `Content` component optionally takes a string containing the HTML of the page content to render. If no content is provided, it will display a loading spinner, otherwise it will render the content by using the special `$html` attribute that can be used to essentially set the `innerHTML` of an element: ```js -let content = pages[id] - ? h3("div.content", { $html: pages[id] }) +const Content = (html) => { + const content = html + ? h3("div.content", { $html: html }) : h3("div.spinner-container", h3("span.spinner")); + return h3("main.col-sm-12.col-md-9", [ + h3("div.card.fluid", h3("div.section", content)), + ]); +}; ``` -If the content has been loaded, the page content will be added as raw HTML to the `div.content` element (no sanitization needed as we are only going to ever render well-known Markdown files), otherwise a spinner will be displayed (until the application is re-rendered anyway). +Now, the key here is that we are only ever going to render "known" pages that are listed in the `labels` object. + +Suppose for example that the `#/overview` page is loaded. The `h3.route.path` in this case is going to be set to `/overview`, which in turns corresponds to an ID of a well-known page (`overview`). + +In a similar way, other well-known pages can easily be mapped to IDs, but it is also important to handle _unknown_ pages (technically I could even pass an URL to a different site containing a malicious markdown page and have it rendered!), and if a page passed in the URL fragment is not present in the `labels` Object, the Overview page will be rendered instead. + +This feature is also handy to automatically load the Overview when no fragment is specified. ### Initialization and post-redraw operations
A docs/uml/h3.sequence.txt

@@ -0,0 +1,40 @@

+@startuml + +title H3 Sequence Diagram + +entity Application +control Component +entity H3 +entity Router +database Store + +Application --> H3 : <b>h3.init()</b> +H3 -> Store : //initialize// +Store -> Store : //execute modules// +H3 -> Store : dispatch($init) +H3 -> Application : preStart() +H3 -> Router : //initialize// +H3 -> Router : start() +Router -> Component : render() +Router -> Store: dispatch($redraw) +Router -> Store: dispatch($navigation) +H3 -> Application : postStart() + +group redraw + Component -> H3 : <b>h3.redraw()</b> + H3 -> Component : redraw() + H3 -> Store: dispatch($redraw) +end + +group navigation + Component -> H3 : <b>h3.navigateTo()</b> + H3 -> Router : processPath() + Router -> Application : //remove all DOM nodes// + Application -> Component : //remove all DOM nodes// + Router -> Component : render() + Router -> Store: dispatch($redraw) + Router -> Store: dispatch($navigation) +end + + +@enduml
M h3.jsh3.js

@@ -1,3 +1,11 @@

+ +/** + * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com> + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ const equal = (obj1, obj2) => { if ( (obj1 === null && obj2 === null) ||

@@ -464,8 +472,12 @@ }

} } -// Simple store based on Storeon -// https://github.com/storeon/storeon/blob/master/index.js +/** + * The code of the following class is heavily based on Storeon + * Modified according to the terms of the MIT License + * <https://github.com/storeon/storeon/blob/master/LICENSE> + * Copyright 2019 Andrey Sitnik <andrey@sitnik.ru> + */ class Store { constructor() { this.events = {};