├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── d.tsconfig.json ├── exampleServer.js ├── examples ├── assets │ ├── images │ │ ├── import-all.png │ │ ├── import-immediate-fetch.png │ │ └── import-pre-fetch.png │ └── styles │ │ ├── checkbox.min.css │ │ ├── grid.min.css │ │ ├── header.min.css │ │ ├── item.min.css │ │ ├── menu.min.css │ │ ├── reset.min.css │ │ ├── segment.min.css │ │ ├── sidebar.min.css │ │ ├── site.min.css │ │ └── table.min.css ├── index.html ├── named-outlet │ ├── content-attr-params.js │ ├── content-basic.js │ ├── content-guards.js │ ├── content-import-byconvention.js │ ├── content-import.js │ ├── content-nested.js │ ├── content-overview.js │ ├── content-prop-params.js │ ├── index.html │ ├── named-outlet-example.js │ └── nested-one.js ├── router │ ├── content-guards.js │ ├── content-named.js │ ├── content-nested-example.js │ ├── content-nested-named-viewone-example.js │ ├── content-nested-named-viewtwo-example.js │ ├── content-nested-viewone-example.js │ ├── content-nested-viewtwo-example.js │ ├── content-nested.js │ ├── content-overview.js │ ├── index.html │ └── router-example.js └── shared │ ├── code-example.js │ ├── common-styles.js │ └── main-menu.js ├── jsconfig.json ├── karma-unit.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── models.js ├── named-routing.js ├── router.js ├── routes-link.js ├── routes-outlet.js ├── routes-route.js └── routes-router.js └── test ├── assets ├── test-dummy-two.js └── test-dummy.js └── unit ├── end2end.js ├── index.html └── routes-route.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 2 | // ESLint configuration 3 | // http://eslint.org/docs/user-guide/configuring 4 | 5 | module.exports = { 6 | extends: ['eslint-config-colscott'], 7 | overrides: [ 8 | { 9 | files: ["*.test.js", "*.spec.js"], 10 | rules: { 11 | "no-unused-expressions": "off" 12 | } 13 | } 14 | ] 15 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | build 3 | dist 4 | node_modules 5 | Untitled-1.txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | node_modules 3 | test 4 | azure-pipelines.yml 5 | karma-unit.conf.js 6 | rollup.config.js 7 | Untitled-1.txt -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("dev-lib-colscott/.prettierrc.js"), 3 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 colscott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWC Router 2 | [![Build Status](https://dev.azure.com/colscott/a-wc-router/_apis/build/status/colscott.a-wc-router?branchName=master)](https://dev.azure.com/colscott/a-wc-router/_build/latest?definitionId=1&branchName=master) 3 | ## Features 4 | - Web Component 5 | - Code splitting/lazy loading 6 | - Declarative routing 7 | - Nested routing 8 | - Named/auxiliary routing and outlets 9 | - Path params 10 | - Guards 11 | - Works with regular anchor tags for links 12 | - Automatically styling active links 13 | - Zero dependencies 14 | 15 | ## Examples 16 | Examples and further documentation can be found in the examples folder and are hosted [here](https://colscott.github.io/a-wc-router/examples/) 17 | 18 | To run the examples locally run: 19 | 20 | node exampleServer.js 21 | 22 | Then enter this url in the browser: 23 | 24 | http://localhost:3000/a-wc-router/examples/router/ 25 | 26 | # Install 27 | 28 | npm i a-wc-router 29 | 30 | # Usage 31 | ## With HTML 32 | 33 | ```html 34 | 35 | 36 | 37 | 38 | 39 | OR use a CDN 40 | 41 | 42 | 43 | 44 | 45 | 46 | Page 1 47 | Page 2 48 | 49 | This content never shows because of the last catch all route 50 | 51 | 52 | 53 | 54 | 55 | 56 | ``` 57 | 58 | ## With Custom Element 59 | 60 | ```js 61 | import '../node_modules/a-wc-router/build/es6-bundled/src/router.js'; 62 | // Or use a CDN 63 | import 'https://unpkg.com/a-wc-router/src/router.js'; 64 | import 'https://cdn.jsdelivr.net/npm/a-wc-router/src/router.js'; 65 | 66 | import './my-page1.js'; 67 | 68 | class MyApp extends HTMLElement { 69 | connectedCallback() { 70 | if (this.isConnected) { 71 | this.innerHTML = ` 72 | Page 1 73 | Page 2 74 | 75 | This content never shows because of the last catch all route 76 | 77 | 78 | 79 | 80 | `; 81 | } 82 | } 83 | } 84 | 85 | customElements.define('my-app', MyApp); 86 | ``` 87 | 88 | # Routing 89 | ## Routing using a router, outlet, routes and HTML anchors 90 | 91 | ```html 92 | 93 | Please click a link 94 | 95 | 96 | 97 | 98 | 99 | .... 100 | click for user-main custom element 101 | click for item-main custom element 102 | ``` 103 | 104 | ## Passing data to routes 105 | 106 | ```html 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 128 | ``` 129 | 130 | ```html 131 | click for user with required param 132 | click for user with optional param 133 | click for user with at least one param 134 | click for user any number of params 135 | click for user with two named params 136 | ``` 137 | 138 | ## Code splitting and eager loading modules for routes 139 | 140 | ```html 141 | 142 | ``` 143 | 144 | ## Code splitting and lazy loading modules for routes 145 | 146 | ```html 147 | 148 | ``` 149 | 150 | ## Nested Routing 151 | 152 | ```html 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | // Code snippet from user-main with nested routing 161 | connectedCallback() { 162 | if (this.connected && !this.innerHTML) { 163 | this.innerHTML = ` 164 | 165 | 166 | 167 | 168 | 169 | `; 170 | } 171 | } 172 | .... 173 | Navigate to the nested route 174 | ``` 175 | 176 | ## Base URL 177 | Routing only takes place if a url also matches the document.baseURI. 178 | 179 | ```html 180 | 181 | 182 | Wont route 183 | Will route 184 | ``` 185 | 186 | # Named outlets (no router or routes required) 187 | ## Routing using named outlets and HTML anchors 188 | 189 | ```html 190 | Please click a link 191 | .... 192 | Assign element to outlet main 193 | Assign element to outlet 194 | ``` 195 | 196 | ## Passing data to named outlets 197 | 198 | ```html 199 | Please click a link 200 | .... 201 | Assign to outlet 202 | ``` 203 | 204 | ## Code splitting and eager loading modules for named outlets 205 | 206 | ```html 207 | Please click a link 208 | .... 209 | Load from /path-to/user-main.js 210 | Load from /path-to/user-main.js 211 | ``` 212 | 213 | # General to Routing and Named Outlets 214 | ## Styling link matching the active routes 215 | 216 | ```html 217 | 222 | 223 | Regular anchor - will route but wont get active status styling 224 | Router link - will route and get active status styling 225 | ``` 226 | 227 | ## Navigating using HTML anchors 228 | 229 | ```html 230 | Regular anchor - will route but wont get active status styling 231 | outer link - will route and get active status styling 232 | ``` 233 | 234 | ## Navigating using events 235 | 236 | ```js 237 | window.dispatchEvent( 238 | new CustomEvent( 239 | 'navigate', { 240 | detail: { 241 | href: '/(user/123' }})); 242 | ``` 243 | 244 | ## Navigating using RouterElement 245 | 246 | ```js 247 | RouterElement.navigate('myUrl'); 248 | RouterElement.navigate('/user/123'); 249 | ``` 250 | 251 | ## Guards 252 | 253 | ```js 254 | window.addEventListener('onRouteLeave', guard); 255 | 256 | guard(event) { 257 | if (document.getElementById('guard').checked) { 258 | // preventDefault to prevent the navigation 259 | event.preventDefault(); 260 | } 261 | } 262 | ``` 263 | 264 | ## Lifecycle Events 265 | ### onRouterAdded 266 | ### onRouteMatch 267 | ### onRouteLeave 268 | ### onRouteNotHandled 269 | ### onRouteCancelled 270 | ### onLinkActiveStatusUpdated 271 | ### onOutletUpdated 272 | 273 | ## Testing 274 | To run tests: 275 | 276 | npm test 277 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '10.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install 20 | npm run build 21 | displayName: 'npm install and build' 22 | 23 | - script: | 24 | npm test 25 | displayName: 'npm test' 26 | -------------------------------------------------------------------------------- /d.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["esnext.array", "esnext", "es2017", "dom"], 5 | "rootDir": "./", 6 | "moduleResolution": "node", 7 | "checkJs": true, 8 | "allowJs": true, 9 | "declaration": true, 10 | "emitDeclarationOnly": true 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /exampleServer.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | var app = express(); 4 | 5 | app.use('/a-wc-router/examples', express.static('examples')); 6 | app.use('/a-wc-router/build', express.static('build')); 7 | app.use('/a-wc-router/src', express.static('src')); 8 | 9 | function spaHandler(req, res) { 10 | res.sendFile(`${__dirname}/examples/${req.params.example}/index.html`); 11 | }; 12 | 13 | function main(req, res) { 14 | res.sendFile(`${__dirname}/examples/index.html`); 15 | }; 16 | 17 | app.get('/a-wc-router/examples/', main); 18 | app.get('/a-wc-router/examples/index.html', main); 19 | app.get('/a-wc-router/examples/:example', spaHandler); 20 | app.get('/a-wc-router/examples/:example/*', spaHandler); 21 | 22 | app.listen(process.env.PORT || 3000); 23 | 24 | console.log(`server started on: http://localhost:${process.env.PORT || 3000}/a-wc-router/examples/`); -------------------------------------------------------------------------------- /examples/assets/images/import-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colscott/a-wc-router/62171609bccb476de90d66b066bcd20748038f61/examples/assets/images/import-all.png -------------------------------------------------------------------------------- /examples/assets/images/import-immediate-fetch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colscott/a-wc-router/62171609bccb476de90d66b066bcd20748038f61/examples/assets/images/import-immediate-fetch.png -------------------------------------------------------------------------------- /examples/assets/images/import-pre-fetch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colscott/a-wc-router/62171609bccb476de90d66b066bcd20748038f61/examples/assets/images/import-pre-fetch.png -------------------------------------------------------------------------------- /examples/assets/styles/checkbox.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Checkbox 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */.ui.checkbox{position:relative;display:inline-block;-webkit-backface-visibility:hidden;backface-visibility:hidden;outline:0;vertical-align:baseline;font-style:normal;min-height:17px;font-size:1rem;line-height:17px;min-width:17px}.ui.checkbox input[type=checkbox],.ui.checkbox input[type=radio]{cursor:pointer;position:absolute;top:0;left:0;opacity:0!important;outline:0;z-index:3;width:17px;height:17px}.ui.checkbox .box,.ui.checkbox label{cursor:auto;position:relative;display:block;padding-left:1.85714em;outline:0;font-size:1em}.ui.checkbox .box:before,.ui.checkbox label:before{position:absolute;top:0;left:0;width:17px;height:17px;content:'';background:#fff;border-radius:.21428571rem;-webkit-transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;border:1px solid #d4d4d5}.ui.checkbox .box:after,.ui.checkbox label:after{position:absolute;font-size:14px;top:0;left:0;width:17px;height:17px;text-align:center;opacity:0;color:rgba(0,0,0,.87);-webkit-transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease}.ui.checkbox label,.ui.checkbox+label{color:rgba(0,0,0,.87);-webkit-transition:color .1s ease;transition:color .1s ease}.ui.checkbox+label{vertical-align:middle}.ui.checkbox .box:hover::before,.ui.checkbox label:hover::before{background:#fff;border-color:rgba(34,36,38,.35)}.ui.checkbox label:hover,.ui.checkbox+label:hover{color:rgba(0,0,0,.8)}.ui.checkbox .box:active::before,.ui.checkbox label:active::before{background:#f9fafb;border-color:rgba(34,36,38,.35)}.ui.checkbox .box:active::after,.ui.checkbox label:active::after{color:rgba(0,0,0,.95)}.ui.checkbox input:active~label{color:rgba(0,0,0,.95)}.ui.checkbox input:focus~.box:before,.ui.checkbox input:focus~label:before{background:#fff;border-color:#96c8da}.ui.checkbox input:focus~.box:after,.ui.checkbox input:focus~label:after{color:rgba(0,0,0,.95)}.ui.checkbox input:focus~label{color:rgba(0,0,0,.95)}.ui.checkbox input:checked~.box:before,.ui.checkbox input:checked~label:before{background:#fff;border-color:rgba(34,36,38,.35)}.ui.checkbox input:checked~.box:after,.ui.checkbox input:checked~label:after{opacity:1;color:rgba(0,0,0,.95)}.ui.checkbox input:not([type=radio]):indeterminate~.box:before,.ui.checkbox input:not([type=radio]):indeterminate~label:before{background:#fff;border-color:rgba(34,36,38,.35)}.ui.checkbox input:not([type=radio]):indeterminate~.box:after,.ui.checkbox input:not([type=radio]):indeterminate~label:after{opacity:1;color:rgba(0,0,0,.95)}.ui.checkbox input:checked:focus~.box:before,.ui.checkbox input:checked:focus~label:before,.ui.checkbox input:not([type=radio]):indeterminate:focus~.box:before,.ui.checkbox input:not([type=radio]):indeterminate:focus~label:before{background:#fff;border-color:#96c8da}.ui.checkbox input:checked:focus~.box:after,.ui.checkbox input:checked:focus~label:after,.ui.checkbox input:not([type=radio]):indeterminate:focus~.box:after,.ui.checkbox input:not([type=radio]):indeterminate:focus~label:after{color:rgba(0,0,0,.95)}.ui.read-only.checkbox,.ui.read-only.checkbox label{cursor:default}.ui.checkbox input[disabled]~.box:after,.ui.checkbox input[disabled]~label,.ui.disabled.checkbox .box:after,.ui.disabled.checkbox label{cursor:default!important;opacity:.5;color:#000}.ui.checkbox input.hidden{z-index:-1}.ui.checkbox input.hidden+label{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ui.radio.checkbox{min-height:15px}.ui.radio.checkbox .box,.ui.radio.checkbox label{padding-left:1.85714em}.ui.radio.checkbox .box:before,.ui.radio.checkbox label:before{content:'';-webkit-transform:none;transform:none;width:15px;height:15px;border-radius:500rem;top:1px;left:0}.ui.radio.checkbox .box:after,.ui.radio.checkbox label:after{border:none;content:''!important;width:15px;height:15px;line-height:15px}.ui.radio.checkbox .box:after,.ui.radio.checkbox label:after{top:1px;left:0;width:15px;height:15px;border-radius:500rem;-webkit-transform:scale(.46666667);transform:scale(.46666667);background-color:rgba(0,0,0,.87)}.ui.radio.checkbox input:focus~.box:before,.ui.radio.checkbox input:focus~label:before{background-color:#fff}.ui.radio.checkbox input:focus~.box:after,.ui.radio.checkbox input:focus~label:after{background-color:rgba(0,0,0,.95)}.ui.radio.checkbox input:indeterminate~.box:after,.ui.radio.checkbox input:indeterminate~label:after{opacity:0}.ui.radio.checkbox input:checked~.box:before,.ui.radio.checkbox input:checked~label:before{background-color:#fff}.ui.radio.checkbox input:checked~.box:after,.ui.radio.checkbox input:checked~label:after{background-color:rgba(0,0,0,.95)}.ui.radio.checkbox input:focus:checked~.box:before,.ui.radio.checkbox input:focus:checked~label:before{background-color:#fff}.ui.radio.checkbox input:focus:checked~.box:after,.ui.radio.checkbox input:focus:checked~label:after{background-color:rgba(0,0,0,.95)}.ui.slider.checkbox{min-height:1.25rem}.ui.slider.checkbox input{width:3.5rem;height:1.25rem}.ui.slider.checkbox .box,.ui.slider.checkbox label{padding-left:4.5rem;line-height:1rem;color:rgba(0,0,0,.4)}.ui.slider.checkbox .box:before,.ui.slider.checkbox label:before{display:block;position:absolute;content:'';-webkit-transform:none;transform:none;border:none!important;left:0;z-index:1;top:.4rem;background-color:rgba(0,0,0,.05);width:3.5rem;height:.21428571rem;border-radius:500rem;-webkit-transition:background .3s ease;transition:background .3s ease}.ui.slider.checkbox .box:after,.ui.slider.checkbox label:after{background:#fff -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:#fff -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:#fff linear-gradient(transparent,rgba(0,0,0,.05));position:absolute;content:''!important;opacity:1;z-index:2;border:none;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;width:1.5rem;height:1.5rem;top:-.25rem;left:0;-webkit-transform:none;transform:none;border-radius:500rem;-webkit-transition:left .3s ease;transition:left .3s ease}.ui.slider.checkbox input:focus~.box:before,.ui.slider.checkbox input:focus~label:before{background-color:rgba(0,0,0,.15);border:none}.ui.slider.checkbox .box:hover,.ui.slider.checkbox label:hover{color:rgba(0,0,0,.8)}.ui.slider.checkbox .box:hover::before,.ui.slider.checkbox label:hover::before{background:rgba(0,0,0,.15)}.ui.slider.checkbox input:checked~.box,.ui.slider.checkbox input:checked~label{color:rgba(0,0,0,.95)!important}.ui.slider.checkbox input:checked~.box:before,.ui.slider.checkbox input:checked~label:before{background-color:#545454!important}.ui.slider.checkbox input:checked~.box:after,.ui.slider.checkbox input:checked~label:after{left:2rem}.ui.slider.checkbox input:focus:checked~.box,.ui.slider.checkbox input:focus:checked~label{color:rgba(0,0,0,.95)!important}.ui.slider.checkbox input:focus:checked~.box:before,.ui.slider.checkbox input:focus:checked~label:before{background-color:#000!important}.ui.toggle.checkbox{min-height:1.5rem}.ui.toggle.checkbox input{width:3.5rem;height:1.5rem}.ui.toggle.checkbox .box,.ui.toggle.checkbox label{min-height:1.5rem;padding-left:4.5rem;color:rgba(0,0,0,.87)}.ui.toggle.checkbox label{padding-top:.15em}.ui.toggle.checkbox .box:before,.ui.toggle.checkbox label:before{display:block;position:absolute;content:'';z-index:1;-webkit-transform:none;transform:none;border:none;top:0;background:rgba(0,0,0,.05);-webkit-box-shadow:none;box-shadow:none;width:3.5rem;height:1.5rem;border-radius:500rem}.ui.toggle.checkbox .box:after,.ui.toggle.checkbox label:after{background:#fff -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:#fff -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:#fff linear-gradient(transparent,rgba(0,0,0,.05));position:absolute;content:''!important;opacity:1;z-index:2;border:none;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;width:1.5rem;height:1.5rem;top:0;left:0;border-radius:500rem;-webkit-transition:background .3s ease,left .3s ease;transition:background .3s ease,left .3s ease}.ui.toggle.checkbox input~.box:after,.ui.toggle.checkbox input~label:after{left:-.05rem;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset}.ui.toggle.checkbox input:focus~.box:before,.ui.toggle.checkbox input:focus~label:before{background-color:rgba(0,0,0,.15);border:none}.ui.toggle.checkbox .box:hover::before,.ui.toggle.checkbox label:hover::before{background-color:rgba(0,0,0,.15);border:none}.ui.toggle.checkbox input:checked~.box,.ui.toggle.checkbox input:checked~label{color:rgba(0,0,0,.95)!important}.ui.toggle.checkbox input:checked~.box:before,.ui.toggle.checkbox input:checked~label:before{background-color:#2185d0!important}.ui.toggle.checkbox input:checked~.box:after,.ui.toggle.checkbox input:checked~label:after{left:2.15rem;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset}.ui.toggle.checkbox input:focus:checked~.box,.ui.toggle.checkbox input:focus:checked~label{color:rgba(0,0,0,.95)!important}.ui.toggle.checkbox input:focus:checked~.box:before,.ui.toggle.checkbox input:focus:checked~label:before{background-color:#0d71bb!important}.ui.fitted.checkbox .box,.ui.fitted.checkbox label{padding-left:0!important}.ui.fitted.toggle.checkbox{width:3.5rem}.ui.fitted.slider.checkbox{width:3.5rem}.ui.inverted.checkbox label,.ui.inverted.checkbox+label{color:rgba(255,255,255,.9)!important}.ui.inverted.checkbox .box:hover,.ui.inverted.checkbox label:hover{color:#fff!important}.ui.inverted.checkbox .box:hover::before,.ui.inverted.checkbox label:hover::before{border-color:rgba(34,36,38,.5)}.ui.inverted.slider.checkbox .box,.ui.inverted.slider.checkbox label{color:rgba(255,255,255,.5)}.ui.inverted.slider.checkbox .box:before,.ui.inverted.slider.checkbox label:before{background-color:rgba(255,255,255,.5)!important}.ui.inverted.slider.checkbox .box:hover::before,.ui.inverted.slider.checkbox label:hover::before{background:rgba(255,255,255,.7)!important}.ui.inverted.slider.checkbox input:checked~.box,.ui.inverted.slider.checkbox input:checked~label{color:#fff!important}.ui.inverted.slider.checkbox input:checked~.box:before,.ui.inverted.slider.checkbox input:checked~label:before{background-color:rgba(255,255,255,.8)!important}.ui.inverted.slider.checkbox input:focus:checked~.box,.ui.inverted.slider.checkbox input:focus:checked~label{color:#fff!important}.ui.inverted.slider.checkbox input:focus:checked~.box:before,.ui.inverted.slider.checkbox input:focus:checked~label:before{background-color:rgba(255,255,255,.8)!important}.ui.inverted.toggle.checkbox .box:before,.ui.inverted.toggle.checkbox label:before{background-color:rgba(255,255,255,.9)!important}.ui.inverted.toggle.checkbox .box:hover::before,.ui.inverted.toggle.checkbox label:hover::before{background:#fff!important}.ui.inverted.toggle.checkbox input:checked~.box,.ui.inverted.toggle.checkbox input:checked~label{color:#fff!important}.ui.inverted.toggle.checkbox input:checked~.box:before,.ui.inverted.toggle.checkbox input:checked~label:before{background-color:#2185d0!important}.ui.inverted.toggle.checkbox input:focus:checked~.box,.ui.inverted.toggle.checkbox input:focus:checked~label{color:#fff!important}.ui.inverted.toggle.checkbox input:focus:checked~.box:before,.ui.inverted.toggle.checkbox input:focus:checked~label:before{background-color:#0d71bb!important}@font-face{font-family:Checkbox;src:url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype')}.ui.checkbox .box:after,.ui.checkbox label:after{font-family:Checkbox}.ui.checkbox input:checked~.box:after,.ui.checkbox input:checked~label:after{content:'\e800'}.ui.checkbox input:indeterminate~.box:after,.ui.checkbox input:indeterminate~label:after{font-size:12px;content:'\e801'} -------------------------------------------------------------------------------- /examples/assets/styles/header.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Header 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */.ui.header{border:none;margin:calc(2rem - .1428571428571429em) 0 1rem;padding:0 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:700;line-height:1.28571429em;text-transform:none;color:rgba(0,0,0,.87)}.ui.header:first-child{margin-top:-.14285714em}.ui.header:last-child{margin-bottom:0}.ui.header .sub.header{display:block;font-weight:400;padding:0;margin:0;font-size:1rem;line-height:1.2em;color:rgba(0,0,0,.6)}.ui.header>.icon{display:table-cell;opacity:1;font-size:1.5em;padding-top:0;vertical-align:middle}.ui.header .icon:only-child{display:inline-block;padding:0;margin-right:.75rem}.ui.header>.image:not(.icon),.ui.header>img{display:inline-block;margin-top:.14285714em;width:2.5em;height:auto;vertical-align:middle}.ui.header>.image:not(.icon):only-child,.ui.header>img:only-child{margin-right:.75rem}.ui.header .content{display:inline-block;vertical-align:top}.ui.header>.image+.content,.ui.header>img+.content{padding-left:.75rem;vertical-align:middle}.ui.header>.icon+.content{padding-left:.75rem;display:table-cell;vertical-align:middle}.ui.header .ui.label{font-size:'';margin-left:.5rem;vertical-align:middle}.ui.header+p{margin-top:0}h1.ui.header{font-size:2rem}h2.ui.header{font-size:1.71428571rem}h3.ui.header{font-size:1.28571429rem}h4.ui.header{font-size:1.07142857rem}h5.ui.header{font-size:1rem}h1.ui.header .sub.header{font-size:1.14285714rem}h2.ui.header .sub.header{font-size:1.14285714rem}h3.ui.header .sub.header{font-size:1rem}h4.ui.header .sub.header{font-size:1rem}h5.ui.header .sub.header{font-size:.92857143rem}.ui.huge.header{min-height:1em;font-size:2em}.ui.large.header{font-size:1.71428571em}.ui.medium.header{font-size:1.28571429em}.ui.small.header{font-size:1.07142857em}.ui.tiny.header{font-size:1em}.ui.huge.header .sub.header{font-size:1.14285714rem}.ui.large.header .sub.header{font-size:1.14285714rem}.ui.header .sub.header{font-size:1rem}.ui.small.header .sub.header{font-size:1rem}.ui.tiny.header .sub.header{font-size:.92857143rem}.ui.sub.header{padding:0;margin-bottom:.14285714rem;font-weight:700;font-size:.85714286em;text-transform:uppercase;color:''}.ui.small.sub.header{font-size:.78571429em}.ui.large.sub.header{font-size:.92857143em}.ui.huge.sub.header{font-size:1em}.ui.icon.header{display:inline-block;text-align:center;margin:2rem 0 1rem}.ui.icon.header:after{content:'';display:block;height:0;clear:both;visibility:hidden}.ui.icon.header:first-child{margin-top:0}.ui.icon.header .icon{float:none;display:block;width:auto;height:auto;line-height:1;padding:0;font-size:3em;margin:0 auto .5rem;opacity:1}.ui.icon.header .corner.icon{font-size:calc(3em * .45)}.ui.icon.header .content{display:block;padding:0}.ui.icon.header .circular.icon{font-size:2em}.ui.icon.header .square.icon{font-size:2em}.ui.block.icon.header .icon{margin-bottom:0}.ui.icon.header.aligned{margin-left:auto;margin-right:auto;display:block}.ui.disabled.header{opacity:.45}.ui.inverted.header{color:#fff}.ui.inverted.header .sub.header{color:rgba(255,255,255,.8)}.ui.inverted.attached.header{background:#1b1c1d;-webkit-box-shadow:none;box-shadow:none;border-color:transparent}.ui.inverted.block.header{background:#545454 -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:#545454 -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:#545454 linear-gradient(transparent,rgba(0,0,0,.05));-webkit-box-shadow:none;box-shadow:none;border-bottom:none}.ui.primary.header{color:#2185d0}a.ui.primary.header:hover{color:#1678c2}.ui.primary.dividing.header{border-bottom:2px solid #2185d0}.ui.inverted.primary.header.header.header{color:#54c8ff}a.ui.inverted.primary.header.header.header:hover{color:#21b8ff}.ui.inverted.primary.dividing.header{border-bottom:2px solid #54c8ff}.ui.secondary.header{color:#1b1c1d}a.ui.secondary.header:hover{color:#27292a}.ui.secondary.dividing.header{border-bottom:2px solid #1b1c1d}.ui.inverted.secondary.header.header.header{color:#545454}a.ui.inverted.secondary.header.header.header:hover{color:#6e6e6e}.ui.inverted.secondary.dividing.header{border-bottom:2px solid #545454}.ui.red.header{color:#db2828}a.ui.red.header:hover{color:#d01919}.ui.red.dividing.header{border-bottom:2px solid #db2828}.ui.inverted.red.header.header.header{color:#ff695e}a.ui.inverted.red.header.header.header:hover{color:#ff392b}.ui.inverted.red.dividing.header{border-bottom:2px solid #ff695e}.ui.orange.header{color:#f2711c}a.ui.orange.header:hover{color:#f26202}.ui.orange.dividing.header{border-bottom:2px solid #f2711c}.ui.inverted.orange.header.header.header{color:#ff851b}a.ui.inverted.orange.header.header.header:hover{color:#e76b00}.ui.inverted.orange.dividing.header{border-bottom:2px solid #ff851b}.ui.yellow.header{color:#fbbd08}a.ui.yellow.header:hover{color:#eaae00}.ui.yellow.dividing.header{border-bottom:2px solid #fbbd08}.ui.inverted.yellow.header.header.header{color:#ffe21f}a.ui.inverted.yellow.header.header.header:hover{color:#ebcd00}.ui.inverted.yellow.dividing.header{border-bottom:2px solid #ffe21f}.ui.olive.header{color:#b5cc18}a.ui.olive.header:hover{color:#a7bd0d}.ui.olive.dividing.header{border-bottom:2px solid #b5cc18}.ui.inverted.olive.header.header.header{color:#d9e778}a.ui.inverted.olive.header.header.header:hover{color:#d2e745}.ui.inverted.olive.dividing.header{border-bottom:2px solid #d9e778}.ui.green.header{color:#21ba45}a.ui.green.header:hover{color:#16ab39}.ui.green.dividing.header{border-bottom:2px solid #21ba45}.ui.inverted.green.header.header.header{color:#2ecc40}a.ui.inverted.green.header.header.header:hover{color:#1ea92e}.ui.inverted.green.dividing.header{border-bottom:2px solid #2ecc40}.ui.teal.header{color:#00b5ad}a.ui.teal.header:hover{color:#009c95}.ui.teal.dividing.header{border-bottom:2px solid #00b5ad}.ui.inverted.teal.header.header.header{color:#6dffff}a.ui.inverted.teal.header.header.header:hover{color:#3affff}.ui.inverted.teal.dividing.header{border-bottom:2px solid #6dffff}.ui.blue.header{color:#2185d0}a.ui.blue.header:hover{color:#1678c2}.ui.blue.dividing.header{border-bottom:2px solid #2185d0}.ui.inverted.blue.header.header.header{color:#54c8ff}a.ui.inverted.blue.header.header.header:hover{color:#21b8ff}.ui.inverted.blue.dividing.header{border-bottom:2px solid #54c8ff}.ui.violet.header{color:#6435c9}a.ui.violet.header:hover{color:#5829bb}.ui.violet.dividing.header{border-bottom:2px solid #6435c9}.ui.inverted.violet.header.header.header{color:#a291fb}a.ui.inverted.violet.header.header.header:hover{color:#745aff}.ui.inverted.violet.dividing.header{border-bottom:2px solid #a291fb}.ui.purple.header{color:#a333c8}a.ui.purple.header:hover{color:#9627ba}.ui.purple.dividing.header{border-bottom:2px solid #a333c8}.ui.inverted.purple.header.header.header{color:#dc73ff}a.ui.inverted.purple.header.header.header:hover{color:#cf40ff}.ui.inverted.purple.dividing.header{border-bottom:2px solid #dc73ff}.ui.pink.header{color:#e03997}a.ui.pink.header:hover{color:#e61a8d}.ui.pink.dividing.header{border-bottom:2px solid #e03997}.ui.inverted.pink.header.header.header{color:#ff8edf}a.ui.inverted.pink.header.header.header:hover{color:#ff5bd1}.ui.inverted.pink.dividing.header{border-bottom:2px solid #ff8edf}.ui.brown.header{color:#a5673f}a.ui.brown.header:hover{color:#975b33}.ui.brown.dividing.header{border-bottom:2px solid #a5673f}.ui.inverted.brown.header.header.header{color:#d67c1c}a.ui.inverted.brown.header.header.header:hover{color:#b0620f}.ui.inverted.brown.dividing.header{border-bottom:2px solid #d67c1c}.ui.grey.header{color:#767676}a.ui.grey.header:hover{color:#838383}.ui.grey.dividing.header{border-bottom:2px solid #767676}.ui.inverted.grey.header.header.header{color:#dcddde}a.ui.inverted.grey.header.header.header:hover{color:#c2c4c5}.ui.inverted.grey.dividing.header{border-bottom:2px solid #dcddde}.ui.black.header{color:#1b1c1d}a.ui.black.header:hover{color:#27292a}.ui.black.dividing.header{border-bottom:2px solid #1b1c1d}.ui.inverted.black.header.header.header{color:#545454}a.ui.inverted.black.header.header.header:hover{color:#000}.ui.inverted.black.dividing.header{border-bottom:2px solid #545454}.ui.left.aligned.header{text-align:left}.ui.right.aligned.header{text-align:right}.ui.center.aligned.header,.ui.centered.header{text-align:center}.ui.justified.header{text-align:justify}.ui.justified.header:after{display:inline-block;content:'';width:100%}.ui.floated.header,.ui[class*="left floated"].header{float:left;margin-top:0;margin-right:.5em}.ui[class*="right floated"].header{float:right;margin-top:0;margin-left:.5em}.ui.fitted.header{padding:0}.ui.dividing.header{padding-bottom:.21428571rem;border-bottom:1px solid rgba(34,36,38,.15)}.ui.dividing.header .sub.header{padding-bottom:.21428571rem}.ui.dividing.header .icon{margin-bottom:0}.ui.inverted.dividing.header{border-bottom-color:rgba(255,255,255,.1)}.ui.block.header{background:#f3f4f5;padding:.78571429rem 1rem;-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5;border-radius:.28571429rem}.ui.tiny.block.header{font-size:.85714286rem}.ui.small.block.header{font-size:.92857143rem}.ui.block.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6){font-size:1rem}.ui.large.block.header{font-size:1.14285714rem}.ui.huge.block.header{font-size:1.42857143rem}.ui.attached.header{background:#fff;padding:.78571429rem 1rem;margin:0 -1px 0 -1px;-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5;border-radius:0}.ui.attached.block.header{background:#f3f4f5}.ui.attached:not(.top).header{border-top:none}.ui.top.attached.header{border-radius:.28571429rem .28571429rem 0 0}.ui.bottom.attached.header{border-radius:0 0 .28571429rem .28571429rem}.ui.tiny.attached.header{font-size:.85714286em}.ui.small.attached.header{font-size:.92857143em}.ui.attached.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6){font-size:1em}.ui.large.attached.header{font-size:1.14285714em}.ui.huge.attached.header{font-size:1.42857143em}.ui.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6){font-size:1.28571429em} -------------------------------------------------------------------------------- /examples/assets/styles/item.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Item 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */.ui.items>.item{display:-webkit-box;display:-ms-flexbox;display:flex;margin:1em 0;width:100%;min-height:0;background:0 0;padding:0;border:none;border-radius:0;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:-webkit-box-shadow .1s ease;transition:-webkit-box-shadow .1s ease;transition:box-shadow .1s ease;transition:box-shadow .1s ease,-webkit-box-shadow .1s ease;z-index:''}.ui.items>.item a{cursor:pointer}.ui.items{margin:1.5em 0}.ui.items:first-child{margin-top:0!important}.ui.items:last-child{margin-bottom:0!important}.ui.items>.item:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.items>.item:first-child{margin-top:0}.ui.items>.item:last-child{margin-bottom:0}.ui.items>.item>.image{position:relative;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;display:block;float:none;margin:0;padding:0;max-height:'';-ms-flex-item-align:start;align-self:start}.ui.items>.item>.image>img{display:block;width:100%;height:auto;border-radius:.125rem;border:none}.ui.items>.item>.image:only-child>img{border-radius:0}.ui.items>.item>.content{display:block;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;background:0 0;color:rgba(0,0,0,.87);margin:0;padding:0;-webkit-box-shadow:none;box-shadow:none;font-size:1em;border:none;border-radius:0}.ui.items>.item>.content:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.items>.item>.image+.content{min-width:0;width:auto;display:block;margin-left:0;-ms-flex-item-align:start;align-self:start;padding-left:1.5em}.ui.items>.item>.content>.header{display:inline-block;margin:-.21425em 0 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:700;color:rgba(0,0,0,.85)}.ui.items>.item>.content>.header:not(.ui){font-size:1.28571429em}.ui.items>.item [class*="left floated"]{float:left}.ui.items>.item [class*="right floated"]{float:right}.ui.items>.item .content img{-ms-flex-item-align:center;align-self:center;width:''}.ui.items>.item .avatar img,.ui.items>.item img.avatar{width:'';height:'';border-radius:500rem}.ui.items>.item>.content>.description{margin-top:.6em;max-width:auto;font-size:1em;line-height:1.4285em;color:rgba(0,0,0,.87)}.ui.items>.item>.content p{margin:0 0 .5em}.ui.items>.item>.content p:last-child{margin-bottom:0}.ui.items>.item .meta{margin:.5em 0 .5em;font-size:1em;line-height:1em;color:rgba(0,0,0,.6)}.ui.items>.item .meta *{margin-right:.3em}.ui.items>.item .meta :last-child{margin-right:0}.ui.items>.item .meta [class*="right floated"]{margin-right:0;margin-left:.3em}.ui.items>.item>.content a:not(.ui){color:'';-webkit-transition:color .1s ease;transition:color .1s ease}.ui.items>.item>.content a:not(.ui):hover{color:''}.ui.items>.item>.content>a.header{color:rgba(0,0,0,.85)}.ui.items>.item>.content>a.header:hover{color:#1e70bf}.ui.items>.item .meta>a:not(.ui){color:rgba(0,0,0,.4)}.ui.items>.item .meta>a:not(.ui):hover{color:rgba(0,0,0,.87)}.ui.items>.item>.content .favorite.icon{cursor:pointer;opacity:.75;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.items>.item>.content .favorite.icon:hover{opacity:1;color:#ffb70a}.ui.items>.item>.content .active.favorite.icon{color:#ffe623}.ui.items>.item>.content .like.icon{cursor:pointer;opacity:.75;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.items>.item>.content .like.icon:hover{opacity:1;color:#ff2733}.ui.items>.item>.content .active.like.icon{color:#ff2733}.ui.items>.item .extra{display:block;position:relative;background:0 0;margin:.5rem 0 0;width:100%;padding:0 0 0;top:0;left:0;color:rgba(0,0,0,.4);-webkit-box-shadow:none;box-shadow:none;-webkit-transition:color .1s ease;transition:color .1s ease;border-top:none}.ui.items>.item .extra>*{margin:.25rem .5rem .25rem 0}.ui.items>.item .extra>[class*="right floated"]{margin:.25rem 0 .25rem .5rem}.ui.items>.item .extra:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.items>.item>.image:not(.ui){width:175px}@media only screen and (min-width:768px) and (max-width:991px){.ui.items>.item{margin:1em 0}.ui.items>.item>.image:not(.ui){width:150px}.ui.items>.item>.image+.content{display:block;padding:0 0 0 1em}}@media only screen and (max-width:767px){.ui.items:not(.unstackable)>.item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:2em 0}.ui.items:not(.unstackable)>.item>.image{display:block;margin-left:auto;margin-right:auto}.ui.items:not(.unstackable)>.item>.image,.ui.items:not(.unstackable)>.item>.image>img{max-width:100%!important;width:auto!important;max-height:250px!important}.ui.items:not(.unstackable)>.item>.image+.content{display:block;padding:1.5em 0 0}}.ui.items>.item>.image+[class*="top aligned"].content{-ms-flex-item-align:start;align-self:flex-start}.ui.items>.item>.image+[class*="middle aligned"].content{-ms-flex-item-align:center;align-self:center}.ui.items>.item>.image+[class*="bottom aligned"].content{-ms-flex-item-align:end;align-self:flex-end}.ui.relaxed.items>.item{margin:1.5em 0}.ui[class*="very relaxed"].items>.item{margin:2em 0}.ui.divided.items>.item{border-top:1px solid rgba(34,36,38,.15);margin:0;padding:1em 0}.ui.divided.items>.item:first-child{border-top:none;margin-top:0!important;padding-top:0!important}.ui.divided.items>.item:last-child{margin-bottom:0!important;padding-bottom:0!important}.ui.relaxed.divided.items>.item{margin:0;padding:1.5em 0}.ui[class*="very relaxed"].divided.items>.item{margin:0;padding:2em 0}.ui.items a.item:hover,.ui.link.items>.item:hover{cursor:pointer}.ui.items a.item:hover .content .header,.ui.link.items>.item:hover .content .header{color:#1e70bf}.ui.items>.item{font-size:1em}@media only screen and (max-width:767px){.ui.unstackable.items>.item>.image,.ui.unstackable.items>.item>.image>img{width:125px!important}}.ui.inverted.items>.item{background:0 0}.ui.inverted.items>.item>.content{background:0 0;color:rgba(255,255,255,.9)}.ui.inverted.items>.item .extra{background:0 0}.ui.inverted.items>.item>.content>.header{color:rgba(255,255,255,.9)}.ui.inverted.items>.item>.content>.description{color:rgba(255,255,255,.9)}.ui.inverted.items>.item .meta{color:rgba(255,255,255,.8)}.ui.inverted.items>.item>.content a:not(.ui){color:#57a4ef}.ui.inverted.items>.item>.content a:not(.ui):hover{color:#4183c4}.ui.inverted.items>.item>.content>a.header{color:rgba(255,255,255,.9)}.ui.inverted.items>.item>.content>a.header:hover{color:#fff}.ui.inverted.items>.item .meta>a:not(.ui){color:rgba(255,255,255,.7)}.ui.inverted.items>.item .meta>a:not(.ui):hover{color:rgba(255,255,255,.9)}.ui.inverted.items>.item>.content .favorite.icon:hover{color:#ffc63d}.ui.inverted.items>.item>.content .active.favorite.icon{color:#ffec56}.ui.inverted.items>.item>.content .like.icon:hover{color:#ff5a63}.ui.inverted.items>.item>.content .active.like.icon{color:#ff5a63}.ui.inverted.items>.item .extra{color:rgba(255,255,255,.7)}.ui.inverted.items a.item:hover .content .header,.ui.inverted.link.items>.item:hover .content .header{color:#fff}.ui.inverted.divided.items>.item{border-top:1px solid rgba(255,255,255,.1)}.ui.inverted.divided.items>.item:first-child{border-top:none} -------------------------------------------------------------------------------- /examples/assets/styles/reset.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Reset 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box}input[type=email],input[type=password],input[type=search],input[type=text]{-webkit-appearance:none;-moz-appearance:none}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none} -------------------------------------------------------------------------------- /examples/assets/styles/segment.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Segment 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */.ui.segment{position:relative;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15);margin:1rem 0;padding:1em 1em;border-radius:.28571429rem;border:1px solid rgba(34,36,38,.15)}.ui.segment:first-child{margin-top:0}.ui.segment:last-child{margin-bottom:0}.ui.vertical.segment{margin:0;padding-left:0;padding-right:0;background:none transparent;border-radius:0;-webkit-box-shadow:none;box-shadow:none;border:none;border-bottom:1px solid rgba(34,36,38,.15)}.ui.vertical.segment:last-child{border-bottom:none}.ui.inverted.segment>.ui.header{color:#fff}.ui[class*="bottom attached"].segment>[class*="top attached"].label{border-top-left-radius:0;border-top-right-radius:0}.ui[class*="top attached"].segment>[class*="bottom attached"].label{border-bottom-left-radius:0;border-bottom-right-radius:0}.ui.attached.segment:not(.top):not(.bottom)>[class*="top attached"].label{border-top-left-radius:0;border-top-right-radius:0}.ui.attached.segment:not(.top):not(.bottom)>[class*="bottom attached"].label{border-bottom-left-radius:0;border-bottom-right-radius:0}.ui.grid>.row>.ui.segment.column,.ui.grid>.ui.segment.column,.ui.page.grid.segment{padding-top:2em;padding-bottom:2em}.ui.grid.segment{margin:1rem 0;border-radius:.28571429rem}.ui.basic.table.segment{background:#fff;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15)}.ui[class*="very basic"].table.segment{padding:1em 1em}.ui.segment.tab:last-child{margin-bottom:1rem}.ui.placeholder.segment{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;max-width:initial;-webkit-animation:none;animation:none;overflow:visible;padding:1em 1em;min-height:18rem;background:#f9fafb;border-color:rgba(34,36,38,.15);-webkit-box-shadow:0 2px 25px 0 rgba(34,36,38,.05) inset;box-shadow:0 2px 25px 0 rgba(34,36,38,.05) inset}.ui.placeholder.segment .button,.ui.placeholder.segment textarea{display:block}.ui.placeholder.segment .button,.ui.placeholder.segment .field,.ui.placeholder.segment textarea,.ui.placeholder.segment>.ui.input{max-width:15rem;margin-left:auto;margin-right:auto}.ui.placeholder.segment .column .button,.ui.placeholder.segment .column .field,.ui.placeholder.segment .column textarea,.ui.placeholder.segment .column>.ui.input{max-width:15rem;margin-left:auto;margin-right:auto}.ui.placeholder.segment>.inline{-ms-flex-item-align:center;align-self:center}.ui.placeholder.segment>.inline>.button{display:inline-block;width:auto;margin:0 .35714286rem 0 0}.ui.placeholder.segment>.inline>.button:last-child{margin-right:0}.ui.piled.segment,.ui.piled.segments{margin:3em 0;-webkit-box-shadow:'';box-shadow:'';z-index:auto}.ui.piled.segment:first-child{margin-top:0}.ui.piled.segment:last-child{margin-bottom:0}.ui.piled.segment:after,.ui.piled.segment:before,.ui.piled.segments:after,.ui.piled.segments:before{background-color:#fff;visibility:visible;content:'';display:block;height:100%;left:0;position:absolute;width:100%;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:'';box-shadow:''}.ui.piled.segment:before,.ui.piled.segments:before{-webkit-transform:rotate(-1.2deg);transform:rotate(-1.2deg);top:0;z-index:-2}.ui.piled.segment:after,.ui.piled.segments:after{-webkit-transform:rotate(1.2deg);transform:rotate(1.2deg);top:0;z-index:-1}.ui[class*="top attached"].piled.segment{margin-top:3em;margin-bottom:0}.ui.piled.segment[class*="top attached"]:first-child{margin-top:0}.ui.piled.segment[class*="bottom attached"]{margin-top:0;margin-bottom:3em}.ui.piled.segment[class*="bottom attached"]:last-child{margin-bottom:0}.ui.stacked.segment{padding-bottom:1.4em}.ui.stacked.segment:after,.ui.stacked.segment:before,.ui.stacked.segments:after,.ui.stacked.segments:before{content:'';position:absolute;bottom:-3px;left:0;border-top:1px solid rgba(34,36,38,.15);background:rgba(0,0,0,.03);width:100%;height:6px;visibility:visible}.ui.stacked.segment:before,.ui.stacked.segments:before{display:none}.ui.tall.stacked.segment:before,.ui.tall.stacked.segments:before{display:block;bottom:0}.ui.stacked.inverted.segment:after,.ui.stacked.inverted.segment:before,.ui.stacked.inverted.segments:after,.ui.stacked.inverted.segments:before{background-color:rgba(0,0,0,.03);border-top:1px solid rgba(34,36,38,.35)}.ui.padded.segment{padding:1.5em}.ui[class*="very padded"].segment{padding:3em}.ui.padded.segment.vertical.segment,.ui[class*="very padded"].vertical.segment{padding-left:0;padding-right:0}.ui.compact.segment{display:table}.ui.compact.segments{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.ui.compact.segments .segment,.ui.segments .compact.segment{display:block;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto}.ui.circular.segment{display:table-cell;padding:2em;text-align:center;vertical-align:middle;border-radius:500em}.ui.raised.raised.segment,.ui.raised.raised.segments{-webkit-box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)}.ui.segments{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;position:relative;margin:1rem 0;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15);border-radius:.28571429rem}.ui.segments:first-child{margin-top:0}.ui.segments:last-child{margin-bottom:0}.ui.segments>.segment{top:0;bottom:0;border-radius:0;margin:0;width:auto;-webkit-box-shadow:none;box-shadow:none;border:none;border-top:1px solid rgba(34,36,38,.15)}.ui.segments:not(.horizontal)>.segment:first-child{top:0;bottom:0;border-top:none;margin-top:0;margin-bottom:0;border-radius:.28571429rem .28571429rem 0 0}.ui.segments:not(.horizontal)>.segment:last-child{top:0;bottom:0;margin-top:0;margin-bottom:0;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;border-radius:0 0 .28571429rem .28571429rem}.ui.segments:not(.horizontal)>.segment:only-child{border-radius:.28571429rem}.ui.segments>.ui.segments{border-top:1px solid rgba(34,36,38,.15);margin:1rem 1rem}.ui.segments>.segments:first-child{border-top:none}.ui.segments>.segment+.segments:not(.horizontal){margin-top:0}.ui.horizontal.segments{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;background-color:transparent;padding:0;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15);margin:1rem 0;border-radius:.28571429rem;border:1px solid rgba(34,36,38,.15)}.ui.stackable.horizontal.segments{-ms-flex-wrap:wrap;flex-wrap:wrap}.ui.segments>.horizontal.segments{margin:0;background-color:transparent;border-radius:0;border:none;-webkit-box-shadow:none;box-shadow:none;border-top:1px solid rgba(34,36,38,.15)}.ui.horizontal.segments>.segment{-webkit-box-flex:1;flex:1 1 auto;-ms-flex:1 1 0;margin:0;min-width:0;background-color:transparent;border-radius:0;border:none;-webkit-box-shadow:none;box-shadow:none;border-left:1px solid rgba(34,36,38,.15)}.ui.segments>.horizontal.segments:first-child{border-top:none}.ui.horizontal.segments:not(.stackable)>.segment:first-child{border-left:none}.ui.disabled.segment{opacity:.45;color:rgba(40,40,40,.3)}.ui.loading.segment{position:relative;cursor:default;pointer-events:none;text-shadow:none!important;-webkit-transition:all 0s linear;transition:all 0s linear}.ui.loading.segment:before{position:absolute;content:'';top:0;left:0;background:rgba(255,255,255,.8);width:100%;height:100%;border-radius:.28571429rem;z-index:100}.ui.loading.segment:after{position:absolute;content:'';top:50%;left:50%;margin:-1.5em 0 0 -1.5em;width:3em;height:3em;-webkit-animation:loader .6s infinite linear;animation:loader .6s infinite linear;border:.2em solid #767676;border-radius:500rem;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent;visibility:visible;z-index:101}.ui.basic.segment,.ui.basic.segments,.ui.segments .ui.basic.segment{background:none transparent;-webkit-box-shadow:none;box-shadow:none;border:none;border-radius:0}.ui.clearing.segment:after{content:".";display:block;height:0;clear:both;visibility:hidden}.ui.red.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #db2828}.ui.inverted.red.segment.segment.segment.segment.segment{background-color:#db2828;color:#fff}.ui.orange.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #f2711c}.ui.inverted.orange.segment.segment.segment.segment.segment{background-color:#f2711c;color:#fff}.ui.yellow.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #fbbd08}.ui.inverted.yellow.segment.segment.segment.segment.segment{background-color:#fbbd08;color:#fff}.ui.olive.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #b5cc18}.ui.inverted.olive.segment.segment.segment.segment.segment{background-color:#b5cc18;color:#fff}.ui.green.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #21ba45}.ui.inverted.green.segment.segment.segment.segment.segment{background-color:#21ba45;color:#fff}.ui.teal.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #00b5ad}.ui.inverted.teal.segment.segment.segment.segment.segment{background-color:#00b5ad;color:#fff}.ui.blue.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #2185d0}.ui.inverted.blue.segment.segment.segment.segment.segment{background-color:#2185d0;color:#fff}.ui.violet.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #6435c9}.ui.inverted.violet.segment.segment.segment.segment.segment{background-color:#6435c9;color:#fff}.ui.purple.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #a333c8}.ui.inverted.purple.segment.segment.segment.segment.segment{background-color:#a333c8;color:#fff}.ui.pink.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #e03997}.ui.inverted.pink.segment.segment.segment.segment.segment{background-color:#e03997;color:#fff}.ui.brown.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #a5673f}.ui.inverted.brown.segment.segment.segment.segment.segment{background-color:#a5673f;color:#fff}.ui.grey.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #767676}.ui.inverted.grey.segment.segment.segment.segment.segment{background-color:#767676;color:#fff}.ui.black.segment.segment.segment.segment.segment:not(.inverted){border-top:2px solid #1b1c1d}.ui.inverted.black.segment.segment.segment.segment.segment{background-color:#1b1c1d;color:#fff}.ui[class*="left aligned"].segment{text-align:left}.ui[class*="right aligned"].segment{text-align:right}.ui[class*="center aligned"].segment{text-align:center}.ui.floated.segment,.ui[class*="left floated"].segment{float:left;margin-right:1em}.ui[class*="right floated"].segment{float:right;margin-left:1em}.ui.inverted.segment{border:none;-webkit-box-shadow:none;box-shadow:none}.ui.inverted.segment,.ui.primary.inverted.segment{background:#1b1c1d;color:rgba(255,255,255,.9)}.ui.inverted.segment .segment{color:rgba(0,0,0,.87)}.ui.inverted.segment .inverted.segment{color:rgba(255,255,255,.9)}.ui.inverted.attached.segment{border-color:#555}.ui.inverted.loading.segment{color:#fff}.ui.inverted.loading.segment:before{background:rgba(0,0,0,.85)}.ui.secondary.segment{background:#f3f4f5;color:rgba(0,0,0,.6)}.ui.secondary.inverted.segment{background:#4c4f52 -webkit-gradient(linear,left top,left bottom,color-stop(0,rgba(255,255,255,.2)),to(rgba(255,255,255,.2)));background:#4c4f52 -webkit-linear-gradient(rgba(255,255,255,.2) 0,rgba(255,255,255,.2) 100%);background:#4c4f52 linear-gradient(rgba(255,255,255,.2) 0,rgba(255,255,255,.2) 100%);color:rgba(255,255,255,.8)}.ui.tertiary.segment{background:#dcddde;color:rgba(0,0,0,.6)}.ui.tertiary.inverted.segment{background:#717579 -webkit-gradient(linear,left top,left bottom,color-stop(0,rgba(255,255,255,.35)),to(rgba(255,255,255,.35)));background:#717579 -webkit-linear-gradient(rgba(255,255,255,.35) 0,rgba(255,255,255,.35) 100%);background:#717579 linear-gradient(rgba(255,255,255,.35) 0,rgba(255,255,255,.35) 100%);color:rgba(255,255,255,.8)}.ui.attached.segment{top:0;bottom:0;border-radius:0;margin:0 -1px;width:calc(100% + 2px);max-width:calc(100% + 2px);-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5}.ui.attached:not(.message)+.ui.attached.segment:not(.top){border-top:none}.ui[class*="top attached"].segment{bottom:0;margin-bottom:0;top:0;margin-top:1rem;border-radius:.28571429rem .28571429rem 0 0}.ui.segment[class*="top attached"]:first-child{margin-top:0}.ui.segment[class*="bottom attached"]{bottom:0;margin-top:0;top:0;margin-bottom:1rem;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;border-radius:0 0 .28571429rem .28571429rem}.ui.segment[class*="bottom attached"]:last-child{margin-bottom:1rem}.ui.mini.segment,.ui.mini.segments .segment{font-size:.78571429rem}.ui.tiny.segment,.ui.tiny.segments .segment{font-size:.85714286rem}.ui.small.segment,.ui.small.segments .segment{font-size:.92857143rem}.ui.segment,.ui.segments .segment{font-size:1rem}.ui.large.segment,.ui.large.segments .segment{font-size:1.14285714rem}.ui.big.segment,.ui.big.segments .segment{font-size:1.28571429rem}.ui.huge.segment,.ui.huge.segments .segment{font-size:1.42857143rem}.ui.massive.segment,.ui.massive.segments .segment{font-size:1.71428571rem} -------------------------------------------------------------------------------- /examples/assets/styles/sidebar.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Sidebar 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */.ui.sidebar{position:fixed;top:0;left:0;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:none;transition:none;will-change:transform;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);visibility:hidden;-webkit-overflow-scrolling:touch;height:100%!important;max-height:100%;border-radius:0!important;margin:0!important;overflow-y:auto!important;z-index:102}.ui.sidebar>*{-webkit-backface-visibility:hidden;backface-visibility:hidden}.ui.left.sidebar{right:auto;left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.sidebar{right:0!important;left:auto!important;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.bottom.sidebar,.ui.top.sidebar{width:100%!important;height:auto!important}.ui.top.sidebar{top:0!important;bottom:auto!important;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.sidebar{top:auto!important;bottom:0!important;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.pushable{height:100%;overflow-x:hidden;padding:0!important}body.pushable{background:#545454!important}.pushable:not(body){-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.pushable:not(body)>.fixed,.pushable:not(body)>.pusher:after,.pushable:not(body)>.ui.sidebar{position:absolute}.pushable>.fixed{position:fixed;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;will-change:transform;z-index:101}.pushable>.pusher{position:relative;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;min-height:100%;-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;z-index:2;background:inherit}body.pushable>.pusher{background:#fff}.pushable>.pusher:after{position:fixed;top:0;right:0;content:'';background-color:rgba(0,0,0,.4);overflow:hidden;opacity:0;-webkit-transition:opacity .5s;transition:opacity .5s;will-change:opacity;z-index:1000}.ui.sidebar.menu .item{border-radius:0!important}.pushable>.pusher.dimmed:after{width:100%!important;height:100%!important;opacity:1!important}.ui.animating.sidebar{visibility:visible}.ui.visible.sidebar{visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.left.visible.sidebar,.ui.right.visible.sidebar{-webkit-box-shadow:0 0 20px rgba(34,36,38,.15);box-shadow:0 0 20px rgba(34,36,38,.15)}.ui.bottom.visible.sidebar,.ui.top.visible.sidebar{-webkit-box-shadow:0 0 20px rgba(34,36,38,.15);box-shadow:0 0 20px rgba(34,36,38,.15)}.ui.visible.left.sidebar~.fixed,.ui.visible.left.sidebar~.pusher{-webkit-transform:translate3d(260px,0,0);transform:translate3d(260px,0,0)}.ui.visible.right.sidebar~.fixed,.ui.visible.right.sidebar~.pusher{-webkit-transform:translate3d(-260px,0,0);transform:translate3d(-260px,0,0)}.ui.visible.top.sidebar~.fixed,.ui.visible.top.sidebar~.pusher{-webkit-transform:translate3d(0,36px,0);transform:translate3d(0,36px,0)}.ui.visible.bottom.sidebar~.fixed,.ui.visible.bottom.sidebar~.pusher{-webkit-transform:translate3d(0,-36px,0);transform:translate3d(0,-36px,0)}.ui.visible.left.sidebar~.ui.visible.right.sidebar~.fixed,.ui.visible.left.sidebar~.ui.visible.right.sidebar~.pusher,.ui.visible.right.sidebar~.ui.visible.left.sidebar~.fixed,.ui.visible.right.sidebar~.ui.visible.left.sidebar~.pusher{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.thin.left.sidebar,.ui.thin.right.sidebar{width:150px}.ui[class*="very thin"].left.sidebar,.ui[class*="very thin"].right.sidebar{width:60px}.ui.left.sidebar,.ui.right.sidebar{width:260px}.ui.wide.left.sidebar,.ui.wide.right.sidebar{width:350px}.ui[class*="very wide"].left.sidebar,.ui[class*="very wide"].right.sidebar{width:475px}.ui.visible.thin.left.sidebar~.fixed,.ui.visible.thin.left.sidebar~.pusher{-webkit-transform:translate3d(150px,0,0);transform:translate3d(150px,0,0)}.ui.visible[class*="very thin"].left.sidebar~.fixed,.ui.visible[class*="very thin"].left.sidebar~.pusher{-webkit-transform:translate3d(60px,0,0);transform:translate3d(60px,0,0)}.ui.visible.wide.left.sidebar~.fixed,.ui.visible.wide.left.sidebar~.pusher{-webkit-transform:translate3d(350px,0,0);transform:translate3d(350px,0,0)}.ui.visible[class*="very wide"].left.sidebar~.fixed,.ui.visible[class*="very wide"].left.sidebar~.pusher{-webkit-transform:translate3d(475px,0,0);transform:translate3d(475px,0,0)}.ui.visible.thin.right.sidebar~.fixed,.ui.visible.thin.right.sidebar~.pusher{-webkit-transform:translate3d(-150px,0,0);transform:translate3d(-150px,0,0)}.ui.visible[class*="very thin"].right.sidebar~.fixed,.ui.visible[class*="very thin"].right.sidebar~.pusher{-webkit-transform:translate3d(-60px,0,0);transform:translate3d(-60px,0,0)}.ui.visible.wide.right.sidebar~.fixed,.ui.visible.wide.right.sidebar~.pusher{-webkit-transform:translate3d(-350px,0,0);transform:translate3d(-350px,0,0)}.ui.visible[class*="very wide"].right.sidebar~.fixed,.ui.visible[class*="very wide"].right.sidebar~.pusher{-webkit-transform:translate3d(-475px,0,0);transform:translate3d(-475px,0,0)}.ui.overlay.sidebar{z-index:102}.ui.left.overlay.sidebar{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.overlay.sidebar{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.top.overlay.sidebar{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.overlay.sidebar{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.animating.ui.overlay.sidebar,.ui.visible.overlay.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.visible.left.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.right.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.top.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.bottom.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.overlay.sidebar~.fixed,.ui.visible.overlay.sidebar~.pusher{-webkit-transform:none!important;transform:none!important}.ui.push.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;z-index:102}.ui.left.push.sidebar{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.push.sidebar{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.top.push.sidebar{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.push.sidebar{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.ui.visible.push.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.uncover.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);z-index:1}.ui.visible.uncover.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.slide.along.sidebar{z-index:1}.ui.left.slide.along.sidebar{-webkit-transform:translate3d(-50%,0,0);transform:translate3d(-50%,0,0)}.ui.right.slide.along.sidebar{-webkit-transform:translate3d(50%,0,0);transform:translate3d(50%,0,0)}.ui.top.slide.along.sidebar{-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}.ui.bottom.slide.along.sidebar{-webkit-transform:translate3d(0,50%,0);transform:translate3d(0,50%,0)}.ui.animating.slide.along.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.visible.slide.along.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.slide.out.sidebar{z-index:1}.ui.left.slide.out.sidebar{-webkit-transform:translate3d(50%,0,0);transform:translate3d(50%,0,0)}.ui.right.slide.out.sidebar{-webkit-transform:translate3d(-50%,0,0);transform:translate3d(-50%,0,0)}.ui.top.slide.out.sidebar{-webkit-transform:translate3d(0,50%,0);transform:translate3d(0,50%,0)}.ui.bottom.slide.out.sidebar{-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}.ui.animating.slide.out.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.visible.slide.out.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.scale.down.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;z-index:102}.ui.left.scale.down.sidebar{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.scale.down.sidebar{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.top.scale.down.sidebar{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.scale.down.sidebar{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.ui.scale.down.left.sidebar~.pusher{-webkit-transform-origin:75% 50%;transform-origin:75% 50%}.ui.scale.down.right.sidebar~.pusher{-webkit-transform-origin:25% 50%;transform-origin:25% 50%}.ui.scale.down.top.sidebar~.pusher{-webkit-transform-origin:50% 75%;transform-origin:50% 75%}.ui.scale.down.bottom.sidebar~.pusher{-webkit-transform-origin:50% 25%;transform-origin:50% 25%}.ui.animating.scale.down>.visible.ui.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.animating.scale.down.sidebar~.pusher,.ui.visible.scale.down.sidebar~.pusher{display:block!important;width:100%;height:100%;overflow:hidden!important}.ui.visible.scale.down.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.scale.down.sidebar~.pusher{-webkit-transform:scale(.75);transform:scale(.75)} -------------------------------------------------------------------------------- /examples/assets/styles/site.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Site 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin);body,html{height:100%}html{font-size:14px}body{margin:0;padding:0;overflow-x:hidden;min-width:320px;background:#fff;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:14px;line-height:1.4285em;color:rgba(0,0,0,.87);font-smoothing:antialiased}h1,h2,h3,h4,h5{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;line-height:1.28571429em;margin:calc(2rem - .1428571428571429em) 0 1rem;font-weight:700;padding:0}h1{min-height:1rem;font-size:2rem}h2{font-size:1.71428571rem}h3{font-size:1.28571429rem}h4{font-size:1.07142857rem}h5{font-size:1rem}h1:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child{margin-top:0}h1:last-child,h2:last-child,h3:last-child,h4:last-child,h5:last-child{margin-bottom:0}p{margin:0 0 1em;line-height:1.4285em}p:first-child{margin-top:0}p:last-child{margin-bottom:0}a{color:#4183c4;text-decoration:none}a:hover{color:#1e70bf;text-decoration:none}::-webkit-selection{background-color:#cce2ff;color:rgba(0,0,0,.87)}::selection{background-color:#cce2ff;color:rgba(0,0,0,.87)}input::-webkit-selection,textarea::-webkit-selection{background-color:rgba(100,100,100,.4);color:rgba(0,0,0,.87)}input::selection,textarea::selection{background-color:rgba(100,100,100,.4);color:rgba(0,0,0,.87)}body ::-webkit-scrollbar{-webkit-appearance:none;width:10px;height:10px}body ::-webkit-scrollbar-track{background:rgba(0,0,0,.1);border-radius:0}body ::-webkit-scrollbar-thumb{cursor:pointer;border-radius:5px;background:rgba(0,0,0,.25);-webkit-transition:color .2s ease;transition:color .2s ease}body ::-webkit-scrollbar-thumb:window-inactive{background:rgba(0,0,0,.15)}body ::-webkit-scrollbar-thumb:hover{background:rgba(128,135,139,.8)}body .ui.inverted:not(.dimmer)::-webkit-scrollbar-track{background:rgba(255,255,255,.1)}body .ui.inverted:not(.dimmer)::-webkit-scrollbar-thumb{background:rgba(255,255,255,.25)}body .ui.inverted:not(.dimmer)::-webkit-scrollbar-thumb:window-inactive{background:rgba(255,255,255,.15)}body .ui.inverted:not(.dimmer)::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.35)} -------------------------------------------------------------------------------- /examples/assets/styles/table.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * # Semantic UI 2.7.2 - Table 3 | * http://github.com/semantic-org/semantic-ui/ 4 | * 5 | * 6 | * Released under the MIT license 7 | * http://opensource.org/licenses/MIT 8 | * 9 | */.ui.table{width:100%;background:#fff;margin:1em 0;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:none;box-shadow:none;border-radius:.28571429rem;text-align:left;vertical-align:middle;color:rgba(0,0,0,.87);border-collapse:separate;border-spacing:0}.ui.table:first-child{margin-top:0}.ui.table:last-child{margin-bottom:0}.ui.table tbody,.ui.table thead{text-align:inherit;vertical-align:inherit}.ui.table td,.ui.table th{-webkit-transition:background .1s ease,color .1s ease;transition:background .1s ease,color .1s ease}.ui.table thead{-webkit-box-shadow:none;box-shadow:none}.ui.table thead th{cursor:auto;background:#f9fafb;text-align:inherit;color:rgba(0,0,0,.87);padding:.92857143em .78571429em;vertical-align:inherit;font-style:none;font-weight:700;text-transform:none;border-bottom:1px solid rgba(34,36,38,.1);border-left:none}.ui.table thead tr>th:first-child{border-left:none}.ui.table thead tr:first-child>th:first-child{border-radius:.28571429rem 0 0 0}.ui.table thead tr:first-child>th:last-child{border-radius:0 .28571429rem 0 0}.ui.table thead tr:first-child>th:only-child{border-radius:.28571429rem .28571429rem 0 0}.ui.table tfoot{-webkit-box-shadow:none;box-shadow:none}.ui.table tfoot th{cursor:auto;border-top:1px solid rgba(34,36,38,.15);background:#f9fafb;text-align:inherit;color:rgba(0,0,0,.87);padding:.78571429em .78571429em;vertical-align:middle;font-style:normal;font-weight:400;text-transform:none}.ui.table tfoot tr>th:first-child{border-left:none}.ui.table tfoot tr:first-child>th:first-child{border-radius:0 0 0 .28571429rem}.ui.table tfoot tr:first-child>th:last-child{border-radius:0 0 .28571429rem 0}.ui.table tfoot tr:first-child>th:only-child{border-radius:0 0 .28571429rem .28571429rem}.ui.table tr td{border-top:1px solid rgba(34,36,38,.1)}.ui.table tr:first-child td{border-top:none}.ui.table tbody+tbody tr:first-child td{border-top:1px solid rgba(34,36,38,.1)}.ui.table td{padding:.78571429em .78571429em;text-align:inherit}.ui.table>.icon{vertical-align:baseline}.ui.table>.icon:only-child{margin:0}.ui.table.segment{padding:0}.ui.table.segment:after{display:none}.ui.table.segment.stacked:after{display:block}@media only screen and (max-width:767px){.ui.table:not(.unstackable){width:100%;padding:0}.ui.table:not(.unstackable) tbody,.ui.table:not(.unstackable) tr,.ui.table:not(.unstackable) tr>td,.ui.table:not(.unstackable) tr>th{display:block!important;width:auto!important}.ui.table:not(.unstackable) thead{display:block}.ui.table:not(.unstackable) tfoot{display:block}.ui.table:not(.unstackable) tr{padding-top:1em;padding-bottom:1em;-webkit-box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important;box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important}.ui.table:not(.unstackable) tr>th,.ui.ui.ui.ui.table:not(.unstackable) tr>td{background:0 0;border:none;padding:.25em .75em;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.table:not(.unstackable) td:first-child,.ui.table:not(.unstackable) th:first-child{font-weight:700}.ui.definition.table:not(.unstackable) thead th:first-child{-webkit-box-shadow:none!important;box-shadow:none!important}}.ui.table .collapsing .image,.ui.table .collapsing .image img{max-width:none}.ui.structured.table{border-collapse:collapse}.ui.structured.table thead th{border-left:none;border-right:none}.ui.structured.sortable.table thead th{border-left:1px solid rgba(34,36,38,.15);border-right:1px solid rgba(34,36,38,.15)}.ui.structured.basic.table th{border-left:none;border-right:none}.ui.structured.celled.table tr td,.ui.structured.celled.table tr th{border-left:1px solid rgba(34,36,38,.1);border-right:1px solid rgba(34,36,38,.1)}.ui.definition.table thead:not(.full-width) th:first-child{pointer-events:none;background:0 0;font-weight:400;color:rgba(0,0,0,.4);-webkit-box-shadow:-1px -1px 0 1px #fff;box-shadow:-1px -1px 0 1px #fff}.ui.definition.table tfoot:not(.full-width) th:first-child{pointer-events:none;background:0 0;font-weight:400;color:rgba(0,0,0,.4);-webkit-box-shadow:1px 1px 0 1px #fff;box-shadow:1px 1px 0 1px #fff}.ui.celled.definition.table thead:not(.full-width) th:first-child{-webkit-box-shadow:0 -1px 0 1px #fff;box-shadow:0 -1px 0 1px #fff}.ui.celled.definition.table tfoot:not(.full-width) th:first-child{-webkit-box-shadow:0 1px 0 1px #fff;box-shadow:0 1px 0 1px #fff}.ui.definition.table tr td.definition,.ui.definition.table tr td:first-child:not(.ignored){background:rgba(0,0,0,.03);font-weight:700;color:rgba(0,0,0,.95);text-transform:'';-webkit-box-shadow:'';box-shadow:'';text-align:'';font-size:1em;padding-left:'';padding-right:''}.ui.definition.table thead:not(.full-width) th:nth-child(2){border-left:1px solid rgba(34,36,38,.15)}.ui.definition.table tfoot:not(.full-width) th:nth-child(2){border-left:1px solid rgba(34,36,38,.15)}.ui.definition.table td:nth-child(2){border-left:1px solid rgba(34,36,38,.15)}.ui.ui.table td.positive,.ui.ui.ui.ui.table tr.positive{-webkit-box-shadow:0 0 0 #a3c293 inset;box-shadow:0 0 0 #a3c293 inset;background:#fcfff5;color:#2c662d}.ui.ui.table td.negative,.ui.ui.ui.ui.table tr.negative{-webkit-box-shadow:0 0 0 #e0b4b4 inset;box-shadow:0 0 0 #e0b4b4 inset;background:#fff6f6;color:#9f3a38}.ui.ui.table td.error,.ui.ui.ui.ui.table tr.error{-webkit-box-shadow:0 0 0 #e0b4b4 inset;box-shadow:0 0 0 #e0b4b4 inset;background:#fff6f6;color:#9f3a38}.ui.ui.table td.warning,.ui.ui.ui.ui.table tr.warning{-webkit-box-shadow:0 0 0 #c9ba9b inset;box-shadow:0 0 0 #c9ba9b inset;background:#fffaf3;color:#573a08}.ui.ui.table td.active,.ui.ui.ui.ui.table tr.active{-webkit-box-shadow:0 0 0 rgba(0,0,0,.87) inset;box-shadow:0 0 0 rgba(0,0,0,.87) inset;background:#e0e0e0;color:rgba(0,0,0,.87)}.ui.table tr td.disabled,.ui.table tr.disabled td,.ui.table tr.disabled:hover,.ui.table tr:hover td.disabled{pointer-events:none;color:rgba(40,40,40,.3)}@media only screen and (max-width:991px){.ui[class*="tablet stackable"].table,.ui[class*="tablet stackable"].table tbody,.ui[class*="tablet stackable"].table tr,.ui[class*="tablet stackable"].table tr>td,.ui[class*="tablet stackable"].table tr>th{display:block!important;width:100%!important}.ui[class*="tablet stackable"].table{padding:0}.ui[class*="tablet stackable"].table thead{display:block}.ui[class*="tablet stackable"].table tfoot{display:block}.ui[class*="tablet stackable"].table tr{padding-top:1em;padding-bottom:1em;-webkit-box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important;box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important}.ui[class*="tablet stackable"].table tr>td,.ui[class*="tablet stackable"].table tr>th{background:0 0;border:none!important;padding:.25em .75em;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.definition[class*="tablet stackable"].table thead th:first-child{-webkit-box-shadow:none!important;box-shadow:none!important}}.ui.table [class*="left aligned"],.ui.table[class*="left aligned"]{text-align:left}.ui.table [class*="center aligned"],.ui.table[class*="center aligned"]{text-align:center}.ui.table [class*="right aligned"],.ui.table[class*="right aligned"]{text-align:right}.ui.table [class*="top aligned"],.ui.table[class*="top aligned"]{vertical-align:top}.ui.table [class*="middle aligned"],.ui.table[class*="middle aligned"]{vertical-align:middle}.ui.table [class*="bottom aligned"],.ui.table[class*="bottom aligned"]{vertical-align:bottom}.ui.table td.collapsing,.ui.table th.collapsing{width:1px;white-space:nowrap}.ui.fixed.table{table-layout:fixed}.ui.fixed.table td,.ui.fixed.table th{overflow:hidden;text-overflow:ellipsis}.ui.table tbody tr td.selectable:hover,.ui.ui.selectable.table tbody tr:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.inverted.table tbody tr td.selectable:hover,.ui.ui.selectable.inverted.table tbody tr:hover{background:rgba(255,255,255,.08);color:#fff}.ui.table tbody tr td.selectable{padding:0}.ui.table tbody tr td.selectable>a:not(.ui){display:block;color:inherit;padding:.78571429em .78571429em}.ui.selectable.table tr:hover td.error,.ui.table tr td.selectable.error:hover,.ui.ui.selectable.table tr.error:hover{background:#ffe7e7;color:#943634}.ui.selectable.table tr:hover td.warning,.ui.table tr td.selectable.warning:hover,.ui.ui.selectable.table tr.warning:hover{background:#fff4e4;color:#493107}.ui.selectable.table tr:hover td.active,.ui.table tr td.selectable.active:hover,.ui.ui.selectable.table tr.active:hover{background:#e0e0e0;color:rgba(0,0,0,.87)}.ui.selectable.table tr:hover td.positive,.ui.table tr td.selectable.positive:hover,.ui.ui.selectable.table tr.positive:hover{background:#f7ffe6;color:#275b28}.ui.selectable.table tr:hover td.negative,.ui.table tr td.selectable.negative:hover,.ui.ui.selectable.table tr.negative:hover{background:#ffe7e7;color:#943634}.ui.attached.table{top:0;bottom:0;border-radius:0;margin:0 -1px;width:calc(100% + 2px);max-width:calc(100% + 2px);-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5}.ui.attached+.ui.attached.table:not(.top){border-top:none}.ui[class*="top attached"].table{bottom:0;margin-bottom:0;top:0;margin-top:1em;border-radius:.28571429rem .28571429rem 0 0}.ui.table[class*="top attached"]:first-child{margin-top:0}.ui[class*="bottom attached"].table{bottom:0;margin-top:0;top:0;margin-bottom:1em;-webkit-box-shadow:none,none;box-shadow:none,none;border-radius:0 0 .28571429rem .28571429rem}.ui[class*="bottom attached"].table:last-child{margin-bottom:0}.ui.striped.table tbody tr:nth-child(2n),.ui.striped.table>tr:nth-child(2n){background-color:rgba(0,0,50,.02)}.ui.inverted.striped.table tbody tr:nth-child(2n),.ui.inverted.striped.table>tr:nth-child(2n){background-color:rgba(255,255,255,.05)}.ui.striped.selectable.selectable.selectable.table tbody tr.active:hover{background:#efefef;color:rgba(0,0,0,.95)}.ui.table [class*="single line"],.ui.table[class*="single line"]{white-space:nowrap}.ui.primary.table{border-top:.2em solid #2185d0}.ui.inverted.primary.table{background-color:#2185d0;color:#fff}.ui.ui.table td.primary,.ui.ui.ui.ui.table tr.primary{-webkit-box-shadow:0 0 0 #1a69a4 inset;box-shadow:0 0 0 #1a69a4 inset;background:#ddf4ff;color:rgba(255,255,255,.9)}.ui.selectable.table tr:hover td.primary,.ui.table tr td.selectable.primary:hover,.ui.ui.selectable.table tr.primary:hover{background:#d3f1ff;color:rgba(255,255,255,.9)}.ui.secondary.table{border-top:.2em solid #1b1c1d}.ui.inverted.secondary.table{background-color:#1b1c1d;color:#fff}.ui.ui.table td.secondary,.ui.ui.ui.ui.table tr.secondary{-webkit-box-shadow:0 0 0 #020203 inset;box-shadow:0 0 0 #020203 inset;background:#ddd;color:rgba(255,255,255,.9)}.ui.selectable.table tr:hover td.secondary,.ui.table tr td.selectable.secondary:hover,.ui.ui.selectable.table tr.secondary:hover{background:#e2e2e2;color:rgba(255,255,255,.9)}.ui.red.table{border-top:.2em solid #db2828}.ui.inverted.red.table{background-color:#db2828;color:#fff}.ui.ui.table td.red,.ui.ui.ui.ui.table tr.red{-webkit-box-shadow:0 0 0 #b21e1e inset;box-shadow:0 0 0 #b21e1e inset;background:#ffe1df;color:#db2828}.ui.selectable.table tr:hover td.red,.ui.table tr td.selectable.red:hover,.ui.ui.selectable.table tr.red:hover{background:#ffd7d5;color:#db2828}.ui.orange.table{border-top:.2em solid #f2711c}.ui.inverted.orange.table{background-color:#f2711c;color:#fff}.ui.ui.table td.orange,.ui.ui.ui.ui.table tr.orange{-webkit-box-shadow:0 0 0 #cf590c inset;box-shadow:0 0 0 #cf590c inset;background:#ffe7d1;color:#f2711c}.ui.selectable.table tr:hover td.orange,.ui.table tr td.selectable.orange:hover,.ui.ui.selectable.table tr.orange:hover{background:#fae1cc;color:#f2711c}.ui.yellow.table{border-top:.2em solid #fbbd08}.ui.inverted.yellow.table{background-color:#fbbd08;color:#fff}.ui.ui.table td.yellow,.ui.ui.ui.ui.table tr.yellow{-webkit-box-shadow:0 0 0 #cd9903 inset;box-shadow:0 0 0 #cd9903 inset;background:#fff9d2;color:#b58105}.ui.selectable.table tr:hover td.yellow,.ui.table tr td.selectable.yellow:hover,.ui.ui.selectable.table tr.yellow:hover{background:#fbf5cc;color:#b58105}.ui.olive.table{border-top:.2em solid #b5cc18}.ui.inverted.olive.table{background-color:#b5cc18;color:#fff}.ui.ui.table td.olive,.ui.ui.ui.ui.table tr.olive{-webkit-box-shadow:0 0 0 #8d9e13 inset;box-shadow:0 0 0 #8d9e13 inset;background:#f7fae4;color:#8abc1e}.ui.selectable.table tr:hover td.olive,.ui.table tr td.selectable.olive:hover,.ui.ui.selectable.table tr.olive:hover{background:#f6fada;color:#8abc1e}.ui.green.table{border-top:.2em solid #21ba45}.ui.inverted.green.table{background-color:#21ba45;color:#fff}.ui.ui.table td.green,.ui.ui.ui.ui.table tr.green{-webkit-box-shadow:0 0 0 #198f35 inset;box-shadow:0 0 0 #198f35 inset;background:#d5f5d9;color:#1ebc30}.ui.selectable.table tr:hover td.green,.ui.table tr td.selectable.green:hover,.ui.ui.selectable.table tr.green:hover{background:#d2eed5;color:#1ebc30}.ui.teal.table{border-top:.2em solid #00b5ad}.ui.inverted.teal.table{background-color:#00b5ad;color:#fff}.ui.ui.table td.teal,.ui.ui.ui.ui.table tr.teal{-webkit-box-shadow:0 0 0 #00827c inset;box-shadow:0 0 0 #00827c inset;background:#e2ffff;color:#10a3a3}.ui.selectable.table tr:hover td.teal,.ui.table tr td.selectable.teal:hover,.ui.ui.selectable.table tr.teal:hover{background:#d8ffff;color:#10a3a3}.ui.blue.table{border-top:.2em solid #2185d0}.ui.inverted.blue.table{background-color:#2185d0;color:#fff}.ui.ui.table td.blue,.ui.ui.ui.ui.table tr.blue{-webkit-box-shadow:0 0 0 #1a69a4 inset;box-shadow:0 0 0 #1a69a4 inset;background:#ddf4ff;color:#2185d0}.ui.selectable.table tr:hover td.blue,.ui.table tr td.selectable.blue:hover,.ui.ui.selectable.table tr.blue:hover{background:#d3f1ff;color:#2185d0}.ui.violet.table{border-top:.2em solid #6435c9}.ui.inverted.violet.table{background-color:#6435c9;color:#fff}.ui.ui.table td.violet,.ui.ui.ui.ui.table tr.violet{-webkit-box-shadow:0 0 0 #502aa1 inset;box-shadow:0 0 0 #502aa1 inset;background:#ece9fe;color:#6435c9}.ui.selectable.table tr:hover td.violet,.ui.table tr td.selectable.violet:hover,.ui.ui.selectable.table tr.violet:hover{background:#e3deff;color:#6435c9}.ui.purple.table{border-top:.2em solid #a333c8}.ui.inverted.purple.table{background-color:#a333c8;color:#fff}.ui.ui.table td.purple,.ui.ui.ui.ui.table tr.purple{-webkit-box-shadow:0 0 0 #82299f inset;box-shadow:0 0 0 #82299f inset;background:#f8e3ff;color:#a333c8}.ui.selectable.table tr:hover td.purple,.ui.table tr td.selectable.purple:hover,.ui.ui.selectable.table tr.purple:hover{background:#f5d9ff;color:#a333c8}.ui.pink.table{border-top:.2em solid #e03997}.ui.inverted.pink.table{background-color:#e03997;color:#fff}.ui.ui.table td.pink,.ui.ui.ui.ui.table tr.pink{-webkit-box-shadow:0 0 0 #c71f7e inset;box-shadow:0 0 0 #c71f7e inset;background:#ffe8f9;color:#e03997}.ui.selectable.table tr:hover td.pink,.ui.table tr td.selectable.pink:hover,.ui.ui.selectable.table tr.pink:hover{background:#ffdef6;color:#e03997}.ui.brown.table{border-top:.2em solid #a5673f}.ui.inverted.brown.table{background-color:#a5673f;color:#fff}.ui.ui.table td.brown,.ui.ui.ui.ui.table tr.brown{-webkit-box-shadow:0 0 0 #805031 inset;box-shadow:0 0 0 #805031 inset;background:#f7e5d2;color:#a5673f}.ui.selectable.table tr:hover td.brown,.ui.table tr td.selectable.brown:hover,.ui.ui.selectable.table tr.brown:hover{background:#efe0cf;color:#a5673f}.ui.grey.table{border-top:.2em solid #767676}.ui.inverted.grey.table{background-color:#767676;color:#fff}.ui.ui.table td.grey,.ui.ui.ui.ui.table tr.grey{-webkit-box-shadow:0 0 0 #5d5d5d inset;box-shadow:0 0 0 #5d5d5d inset;background:#dcddde;color:#767676}.ui.selectable.table tr:hover td.grey,.ui.table tr td.selectable.grey:hover,.ui.ui.selectable.table tr.grey:hover{background:#c2c4c5;color:#767676}.ui.black.table{border-top:.2em solid #1b1c1d}.ui.inverted.black.table{background-color:#1b1c1d;color:#fff}.ui.ui.table td.black,.ui.ui.ui.ui.table tr.black{-webkit-box-shadow:0 0 0 #020203 inset;box-shadow:0 0 0 #020203 inset;background:#545454;color:#fff}.ui.selectable.table tr:hover td.black,.ui.table tr td.selectable.black:hover,.ui.ui.selectable.table tr.black:hover{background:#000;color:#fff}.ui.one.column.table td{width:100%}.ui.two.column.table td{width:50%}.ui.three.column.table td{width:33.33333333%}.ui.four.column.table td{width:25%}.ui.five.column.table td{width:20%}.ui.six.column.table td{width:16.66666667%}.ui.seven.column.table td{width:14.28571429%}.ui.eight.column.table td{width:12.5%}.ui.nine.column.table td{width:11.11111111%}.ui.ten.column.table td{width:10%}.ui.eleven.column.table td{width:9.09090909%}.ui.twelve.column.table td{width:8.33333333%}.ui.thirteen.column.table td{width:7.69230769%}.ui.fourteen.column.table td{width:7.14285714%}.ui.fifteen.column.table td{width:6.66666667%}.ui.sixteen.column.table td{width:6.25%}.ui.table td.one.wide,.ui.table th.one.wide{width:6.25%}.ui.table td.two.wide,.ui.table th.two.wide{width:12.5%}.ui.table td.three.wide,.ui.table th.three.wide{width:18.75%}.ui.table td.four.wide,.ui.table th.four.wide{width:25%}.ui.table td.five.wide,.ui.table th.five.wide{width:31.25%}.ui.table td.six.wide,.ui.table th.six.wide{width:37.5%}.ui.table td.seven.wide,.ui.table th.seven.wide{width:43.75%}.ui.table td.eight.wide,.ui.table th.eight.wide{width:50%}.ui.table td.nine.wide,.ui.table th.nine.wide{width:56.25%}.ui.table td.ten.wide,.ui.table th.ten.wide{width:62.5%}.ui.table td.eleven.wide,.ui.table th.eleven.wide{width:68.75%}.ui.table td.twelve.wide,.ui.table th.twelve.wide{width:75%}.ui.table td.thirteen.wide,.ui.table th.thirteen.wide{width:81.25%}.ui.table td.fourteen.wide,.ui.table th.fourteen.wide{width:87.5%}.ui.table td.fifteen.wide,.ui.table th.fifteen.wide{width:93.75%}.ui.table td.sixteen.wide,.ui.table th.sixteen.wide{width:100%}.ui.sortable.table thead th{cursor:pointer;white-space:nowrap;border-left:1px solid rgba(34,36,38,.15);color:rgba(0,0,0,.87)}.ui.sortable.table thead th:first-child{border-left:none}.ui.sortable.table thead th.sorted,.ui.sortable.table thead th.sorted:hover{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ui.sortable.table thead th:after{display:none;font-style:normal;font-weight:400;text-decoration:inherit;content:'';height:1em;width:auto;opacity:.8;margin:0 0 0 .5em;font-family:Icons}.ui.sortable.table thead th.ascending:after{content:'\f0d8'}.ui.sortable.table thead th.descending:after{content:'\f0d7'}.ui.sortable.table th.disabled:hover{cursor:auto;color:rgba(40,40,40,.3)}.ui.sortable.table thead th:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.8)}.ui.sortable.table thead th.sorted{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.sortable.table thead th.sorted:after{display:inline-block}.ui.sortable.table thead th.sorted:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.inverted.sortable.table thead th.sorted{background:rgba(255,255,255,.15) -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:rgba(255,255,255,.15) -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:rgba(255,255,255,.15) linear-gradient(transparent,rgba(0,0,0,.05));color:#fff}.ui.inverted.sortable.table thead th:hover{background:rgba(255,255,255,.08) -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:rgba(255,255,255,.08) -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:rgba(255,255,255,.08) linear-gradient(transparent,rgba(0,0,0,.05));color:#fff}.ui.inverted.sortable.table thead th{border-left-color:transparent;border-right-color:transparent}.ui.inverted.table{background:#333;color:rgba(255,255,255,.9);border:none}.ui.inverted.table th{background-color:rgba(0,0,0,.15);border-color:rgba(255,255,255,.1);color:rgba(255,255,255,.9)}.ui.inverted.table tr td{border-color:rgba(255,255,255,.1)}.ui.inverted.table tr td.disabled,.ui.inverted.table tr.disabled td,.ui.inverted.table tr.disabled:hover td,.ui.inverted.table tr:hover td.disabled{pointer-events:none;color:rgba(225,225,225,.3)}.ui.inverted.definition.table tfoot:not(.full-width) th:first-child,.ui.inverted.definition.table thead:not(.full-width) th:first-child{background:#fff}.ui.inverted.definition.table tr td:first-child{background:rgba(255,255,255,.02);color:#fff}.ui.collapsing.table{width:auto}.ui.basic.table{background:0 0;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:none;box-shadow:none}.ui.basic.table tfoot,.ui.basic.table thead{-webkit-box-shadow:none;box-shadow:none}.ui.basic.table th{background:0 0;border-left:none}.ui.basic.table tbody tr{border-bottom:1px solid rgba(0,0,0,.1)}.ui.basic.table td{background:0 0}.ui.basic.striped.table tbody tr:nth-child(2n){background-color:rgba(0,0,0,.05)}.ui[class*="very basic"].table{border:none}.ui[class*="very basic"].table:not(.sortable):not(.striped) td,.ui[class*="very basic"].table:not(.sortable):not(.striped) th{padding:''}.ui[class*="very basic"].table:not(.sortable):not(.striped) td:first-child,.ui[class*="very basic"].table:not(.sortable):not(.striped) th:first-child{padding-left:0}.ui[class*="very basic"].table:not(.sortable):not(.striped) td:last-child,.ui[class*="very basic"].table:not(.sortable):not(.striped) th:last-child{padding-right:0}.ui[class*="very basic"].table:not(.sortable):not(.striped) thead tr:first-child th{padding-top:0}.ui.celled.table tr td,.ui.celled.table tr th{border-left:1px solid rgba(34,36,38,.1)}.ui.celled.table tr td:first-child,.ui.celled.table tr th:first-child{border-left:none}.ui.padded.table th{padding-left:1em;padding-right:1em}.ui.padded.table td,.ui.padded.table th{padding:1em 1em}.ui[class*="very padded"].table th{padding-left:1.5em;padding-right:1.5em}.ui[class*="very padded"].table td{padding:1.5em 1.5em}.ui.compact.table th{padding-left:.7em;padding-right:.7em}.ui.compact.table td{padding:.5em .7em}.ui[class*="very compact"].table th{padding-left:.6em;padding-right:.6em}.ui[class*="very compact"].table td{padding:.4em .6em}.ui.small.table{font-size:.9em}.ui.table{font-size:1em}.ui.large.table{font-size:1.1em} -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/named-outlet/content-attr-params.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './content-basic.js'; 2 | 3 | class ContentTwoElement extends ContentBasicElement { 4 | 5 | constructor() { 6 | super(); 7 | this.description = ` 8 | Extends the previous exmaple by passing parameters (HTML attributes) to the customer element assigned to the outlet. 9 | `; 10 | } 11 | } 12 | 13 | customElements.define('content-attr-params', ContentTwoElement); -------------------------------------------------------------------------------- /examples/named-outlet/content-basic.js: -------------------------------------------------------------------------------- 1 | export class ContentBasicElement extends HTMLElement { 2 | 3 | connectedCallback() { 4 | if (!this._initialized) { 5 | this._initialized = true; 6 | this.render(); 7 | } 8 | } 9 | 10 | static get observedAttributes() { return ['param1', 'param2']; } 11 | 12 | attributeChangedCallback(name, oldValue, newValue) { 13 | this.render(); 14 | } 15 | 16 | constructor() { 17 | super(); 18 | 19 | this.description = ` 20 |

21 | A simple example that assigns/injects a native custom element into a named outlet. 22 | If the named outlet exists the custom element will be rendered in th eoutlet. 23 | If the outlet does not exist the assignment is still made and if/when the named outlet first added to the DOM it the assignment will take place. The assignment will wait for the named outlet to exist. 24 |

25 | `; 26 | } 27 | 28 | render() { 29 | this.innerHTML = ` 30 |
31 |
Description
32 | ${this.description} 33 |
34 | 35 |
36 |
37 | Paramters Passed 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
TypeNameValue
HTML Attributeparam1${this.getAttribute('param1') || 'not set'}
HTML Attributeparam2${this.getAttribute('param2') || 'not set'}
Object Instance propertyparam3${this.param3 || 'not set'}
Object Instance propertyparam4${this.param4 || 'not set'}
70 |
71 | 72 | `; 73 | } 74 | } 75 | 76 | customElements.define('content-basic', ContentBasicElement); -------------------------------------------------------------------------------- /examples/named-outlet/content-guards.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './content-basic.js'; 2 | 3 | class ContentGuardsElement extends ContentBasicElement { 4 | 5 | connectedCallback() { 6 | super.connectedCallback && super.connectedCallback(); 7 | 8 | window.addEventListener('onRouteLeave', this.guard); 9 | } 10 | 11 | disconnectedCallback() { 12 | super.disconnectedCallback && super.disconnectedCallback(); 13 | 14 | window.removeEventListener('onRouteLeave', this.guard); 15 | } 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.guard = this.guard.bind(this); 21 | 22 | this.description = ` 23 |
24 | 25 | 26 |


27 |

28 | Will not be able to leave this route whilst checkbox is enabled. 29 |

30 |

Guard code

31 |
32 |         window.addEventListener('onRouteLeave', guard);
33 | 
34 |         guard(event) {
35 |             if (document.getElementById('guard').checked) {
36 |                 event.preventDefault();
37 |             }
38 |         }
39 |         
40 | `; 41 | } 42 | 43 | guard(event) { 44 | if (document.getElementById('guard').checked) { 45 | event.preventDefault(); 46 | } 47 | } 48 | } 49 | 50 | customElements.define('content-guards', ContentGuardsElement); -------------------------------------------------------------------------------- /examples/named-outlet/content-import-byconvention.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './content-basic.js'; 2 | 3 | class ContentImportByConventionElement extends ContentBasicElement { 4 | 5 | constructor() { 6 | super(); 7 | 8 | this.description = ` 9 |

10 | So far we've seen import links where the url explicitly defines the names of the custom element and the optional script file to import that contains the custom element. This can get a little much to type. Consider the following: 11 |

12 |
<a is="router-link" href="(main:content-import-byconvention(/a-wc-router/examples/named-outlet/content-import-byconvention.js))">Import By Convention</a>

13 |

14 | This can be simplified. You can also create links that import custom elements by using a convention based approach. This is an optional opinionated approach but is more terse.
15 | This will work with kebab case script names that are exactly the same as the custom element they define (with the .js extension). 16 |

17 |
<a is="router-link" href="(main:/a-wc-router/examples/named-outlet/content-import-byconvention)">Import By Convention</a>

18 |

19 | With some creative routing to the script files on the server, you could end up with something like: 20 |

21 |
<a is="router-link" href="(main:/admin/user-details:id=3)">Import By Convention</a>

22 | `; 23 | } 24 | } 25 | 26 | customElements.define('content-import-byconvention', ContentImportByConventionElement); -------------------------------------------------------------------------------- /examples/named-outlet/content-import.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './content-basic.js'; 2 | 3 | class ContentImportElement extends ContentBasicElement { 4 | 5 | constructor() { 6 | super(); 7 | 8 | this.description = ` 9 |

10 | Note that absolute paths must be used. This is due to the import being performed in the router code so the relative path will be relative to the location of the router script. 11 |

(/a-wc-router/examples/named-outlet/content-import.js)

12 | The placement of the import source comes just after the tag name in the url. 13 |

14 |

15 | Example of defered importing (upfront and pre-cache). This helps users implement the PRPL pattern.
16 | We want the first page of a web application to load as fast as possible. In order to do this we want to only load the resources required to render the first page and do so as fast as possible and defer everything that's not required for first render.
17 | Importing a custom element script allows us to defer the loading of the script. 18 |

19 |

20 | Named outlets automatically provide two types of defered importing: 21 |

    22 |
  1. 23 | The import is required immediately in order to render the page. The custom element script is imported as soon as possbile.
    24 | In the screen grab below, the last script was done via an import and was loaded BEFORE the page load because it was needed to render the page. 25 |
    26 | 27 |
    28 |
  2. 29 |
  3. 30 | The import is not currently required to render the page but a link exists on the page containing the import. Here we pre-cache the script so when the user clicks the link the script is already downloaded. However, the import is not critical so the import is made after the first page load but before the user can click the link. This is part of PRPL design.
    31 | In the screen grab below, the last script was done via an import and was loaded AFTER the page load because it was not needed to render the page but it was still loaded so it is ready if the user clocks the link for that page. 32 | 33 |
    34 | 35 |
    36 |
  4. 37 |
38 |

39 |

40 | Here is how the network time line would look if we used the import feature for all of the links on this page: 41 |

42 | 43 |
44 | The current page being loaded was the Basic Outlet Assignment page. You can see that the content-basic.js was loaded as soon as possible. The other scripts used for the pages not rendered were loaded after the page was ready. 45 |

46 | `; 47 | } 48 | } 49 | 50 | customElements.define('content-import', ContentImportElement); -------------------------------------------------------------------------------- /examples/named-outlet/content-nested.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './content-basic.js'; 2 | 3 | class ContentNestedElement extends ContentBasicElement { 4 | 5 | constructor() { 6 | super(); 7 | this.description = ` 8 |

9 | 10 |

11 | `; 12 | } 13 | } 14 | 15 | customElements.define('content-nested', ContentNestedElement); -------------------------------------------------------------------------------- /examples/named-outlet/content-overview.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './content-basic.js'; 2 | 3 | class ContentOverviewElement extends ContentBasicElement { 4 | 5 | constructor() { 6 | super(); 7 | this.description = ` 8 |

9 | The syntax of a links href attribute targeting named outlets as follows: 10 |

11 | Basic Example:
12 | <a href="(outlet-name:your-tag)" >link</a> 13 |

14 |

15 | Complete Example:
16 |

<a href="(outlet-name:your-tag(/path/to/script.js):attr-name=attrValue&.propName=propValue)" >link</a>
17 |

18 |

19 | Syntax:
20 |

<a href="({outlet-name}:{tag}[{(import-path)}]:[attr-name=attrValue&.prop-name=propValue...])" >
21 |

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
NameRequiredDescriptionExample
outlet-nameRequiredThe name of the outlet whose content will be replaced with the custom element.
tagRequiredThe tag name to insert into the named outlet
import-pathOptionalConfigure if the script that defines the custom element is to be loaded on demand. Path should be absolute and wrapped in parentheses(/full/path/yourelement.js)
ParametersOptionalParameters to be set on the custom element. Can be HTML attributes or JavaScript instance properties. NOTE: the outlet rendering will not replace the content if it is being replaced with the same custom element tag. Instead it will update the paramters. Make your custom element tracks attribute and property changes and performs rendering.my-attribute=value1&.myProperty=value2
58 |

59 | `; 60 | } 61 | } 62 | 63 | customElements.define('content-overview', ContentOverviewElement); -------------------------------------------------------------------------------- /examples/named-outlet/content-prop-params.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './content-basic.js'; 2 | 3 | class ContentPropParamsElement extends ContentBasicElement { 4 | 5 | constructor() { 6 | super(); 7 | this.description = ` 8 | Extends the previous exmaple by passing parameters (Object properties) to the customer element assigned to the outlet. 9 | `; 10 | } 11 | } 12 | 13 | customElements.define('content-prop-params', ContentPropParamsElement); -------------------------------------------------------------------------------- /examples/named-outlet/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/named-outlet/named-outlet-example.js: -------------------------------------------------------------------------------- 1 | // import { RouterElement } from '../../build/es6-bundled/src/router.js' 2 | // import '../../src/routes-link.js'; 3 | import '../../src/routes-outlet.js'; 4 | import '../../src/routes-link.js'; 5 | 6 | import './../shared/main-menu.js'; 7 | import './../shared/code-example.js'; 8 | 9 | import './content-overview.js'; 10 | import './content-basic.js'; 11 | import './content-nested.js'; 12 | import './content-attr-params.js'; 13 | import './content-prop-params.js'; 14 | import './content-guards.js'; 15 | 16 | class NamedOutletExampleElement extends HTMLElement { 17 | 18 | connectedCallback() { 19 | if (!this._initialized) { 20 | this._initialized = true; 21 | this.render(); 22 | 23 | // Register links so they can have active state for styling 24 | // RouterElement.registerLinks(this.querySelectorAll('.ui.menu a')); 25 | // window.dispatchEvent( 26 | // new CustomEvent( 27 | // 'routerLinksAdded', { 28 | // detail: { 29 | // links: this.querySelectorAll('[code-example] a') }})); 30 | } 31 | } 32 | 33 | constructor(){ 34 | super(); 35 | } 36 | 37 | render() { 38 | this.innerHTML = ` 39 | 40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 | 58 |

Outlet not assigned yet.

Please click a link above to assign content to this outlet.

59 |
60 |
61 |
62 | `; 63 | } 64 | } 65 | 66 | customElements.define('named-oulet-example', NamedOutletExampleElement); -------------------------------------------------------------------------------- /examples/named-outlet/nested-one.js: -------------------------------------------------------------------------------- 1 | class NestedOne extends HTMLElement { 2 | 3 | connectedCallback() { 4 | this.innerHTML = ` 5 |

6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

22 | `; 23 | } 24 | 25 | constructor() { 26 | super(); 27 | } 28 | } -------------------------------------------------------------------------------- /examples/router/content-guards.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './../named-outlet/content-basic.js'; 2 | 3 | class ContentGuardsElement extends ContentBasicElement { 4 | 5 | connectedCallback() { 6 | super.connectedCallback && super.connectedCallback(); 7 | 8 | window.addEventListener('onRouteLeave', this.guard); 9 | } 10 | 11 | disconnectedCallback() { 12 | super.disconnectedCallback && super.disconnectedCallback(); 13 | 14 | window.removeEventListener('onRouteLeave', this.guard); 15 | } 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.guard = this.guard.bind(this); 21 | 22 | this.description = ` 23 |
24 | 25 | 26 |


27 |

28 | Will not be able to leave this route whilst checkbox is enabled. 29 |

30 |

Guard code

31 |
32 | window.addEventListener('onRouteLeave', guard);
33 | 
34 | guard(event) {
35 |     if (document.getElementById('guard').checked) {
36 |         event.preventDefault();
37 |     }
38 | }
39 | 
40 | `; 41 | } 42 | 43 | guard(event) { 44 | if (document.getElementById('guard').checked) { 45 | event.preventDefault(); 46 | } 47 | } 48 | } 49 | 50 | customElements.define('content-guards', ContentGuardsElement); -------------------------------------------------------------------------------- /examples/router/content-named.js: -------------------------------------------------------------------------------- 1 | import './content-nested-named-viewone-example.js'; 2 | import './content-nested-named-viewtwo-example.js'; 3 | 4 | class ContentNamed extends HTMLElement { 5 | 6 | connectedCallback() { 7 | this.innerHTML = ` 8 |

9 | This view contains a named router. There are two links below. Both links use named relative URLs to target the nested router. 10 |

11 | <a is="router-link" href="(nestedrouter:/view_one)">Nested View 1</a>
12 | <a is="router-link" href="(nestedrouter:/view_two)">Nested View 2</a>
13 |             
14 | The href is made up of (RouterName:/RoutePath).
15 | You can have as many levels of nestings as you require.
16 | Using named routers is a good way to have Routers on different parts of the page (auxiliary reouters). 17 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |

30 | `; 31 | } 32 | 33 | constructor() { 34 | super(); 35 | } 36 | } 37 | 38 | customElements.define('content-named', ContentNamed); -------------------------------------------------------------------------------- /examples/router/content-nested-example.js: -------------------------------------------------------------------------------- 1 | class ContenNestedExample extends HTMLElement { 2 | 3 | connectedCallback() { 4 | this.innerHTML = ` 5 |

6 | Content ${this.getAttribute('pagenum')} 7 |

8 | `; 9 | } 10 | 11 | constructor() { 12 | super(); 13 | } 14 | } 15 | 16 | customElements.define('content-nested-example', ContenNestedExample); -------------------------------------------------------------------------------- /examples/router/content-nested-named-viewone-example.js: -------------------------------------------------------------------------------- 1 | class ContenNestedViewoneExample extends HTMLElement { 2 | 3 | connectedCallback() { 4 | this.innerHTML = ` 5 |

6 |

View One - targeted via a named router

7 | Example of a named nested routing. Only the path of the route being targeted is required as the router is targeted by name.
8 | In this case:
9 | Router Name -> nestedrouter 10 | Route -> view_one 11 | So the link was -> href="(nestedrouter:view_one)" 12 |

13 | `; 14 | } 15 | 16 | constructor() { 17 | super(); 18 | } 19 | } 20 | 21 | customElements.define('content-nested-named-viewone-example', ContenNestedViewoneExample); -------------------------------------------------------------------------------- /examples/router/content-nested-named-viewtwo-example.js: -------------------------------------------------------------------------------- 1 | class ContenNestedViewtwoExample extends HTMLElement { 2 | 3 | connectedCallback() { 4 | this.innerHTML = ` 5 |

6 |

View Two - targeted via a named router

7 | Example of a named nested routing. Only the path of the route being targeted is required as the router is targeted by name.
8 | In this case:
9 | Router Name -> nestedrouter 10 | Route -> view_two 11 | So the link was -> href="(nestedrouter:view_two)" 12 |

13 | `; 14 | } 15 | 16 | constructor() { 17 | super(); 18 | } 19 | } 20 | 21 | customElements.define('content-nested-named-viewtwo-example', ContenNestedViewtwoExample); -------------------------------------------------------------------------------- /examples/router/content-nested-viewone-example.js: -------------------------------------------------------------------------------- 1 | class ContenNestedViewoneExample extends HTMLElement { 2 | 3 | connectedCallback() { 4 | this.innerHTML = ` 5 |

6 |

Nested View One

7 | Example of a nested routing. The full URL most be supplied that includes any parent routes.
8 | In this case:
9 | Base URL -> /a-wc-router/examples/router/ 10 | Parent route -> /nested
11 | Child route -> /viewone
12 | So the link was -> /a-wc-router/examples/router/nested/viewone 13 |

14 | `; 15 | } 16 | 17 | constructor() { 18 | super(); 19 | } 20 | } 21 | 22 | customElements.define('content-nested-viewone-example', ContenNestedViewoneExample); -------------------------------------------------------------------------------- /examples/router/content-nested-viewtwo-example.js: -------------------------------------------------------------------------------- 1 | class ContenNestedViewtwoExample extends HTMLElement { 2 | 3 | connectedCallback() { 4 | this.innerHTML = ` 5 |

6 |

Nested View Two

7 | Example of a nested routing. The full URL most be supplied that includes any parent routes.
8 | In this case:
9 | Base URL -> /a-wc-router/examples/router/ 10 | Parent route -> /nested
11 | Child route -> /viewtwo
12 | So the link was -> /a-wc-router/examples/router/nested/viewtwo 13 |

14 | `; 15 | } 16 | 17 | constructor() { 18 | super(); 19 | } 20 | } 21 | 22 | customElements.define('content-nested-viewtwo-example', ContenNestedViewtwoExample); -------------------------------------------------------------------------------- /examples/router/content-nested.js: -------------------------------------------------------------------------------- 1 | import './content-nested-viewone-example.js'; 2 | import './content-nested-viewtwo-example.js'; 3 | 4 | class ContentNested extends HTMLElement { 5 | 6 | connectedCallback() { 7 | this.innerHTML = ` 8 |

9 | This view contains a nested router. There are two links below. Both links have the full URL specified to target the nested router. 10 |

11 | <a is="router-link" href="/a-wc-router/examples/router/nested/view_one">Nested View 1</a>
12 | <a is="router-link" href="/a-wc-router/examples/router/nested/view_two">Nested View 2</a>
13 |         
14 | The href is made up of BaseUrl + ParentRouterUrl + NestedRouterUrl.
15 | You can have as many levels of nestings as you require. 16 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |

29 | `; 30 | } 31 | 32 | constructor() { 33 | super(); 34 | } 35 | } 36 | 37 | customElements.define('content-nested', ContentNested); -------------------------------------------------------------------------------- /examples/router/content-overview.js: -------------------------------------------------------------------------------- 1 | import { ContentBasicElement } from './../named-outlet/content-basic.js'; 2 | 3 | class ContentOverviewElement extends ContentBasicElement { 4 | 5 | constructor() { 6 | super(); 7 | this.description = ` 8 |

9 | If a URL starts with the base URL of the page then it is a candidate for client side routing. 10 | Routes are set up like in the code above. You can have as many routes as required. 11 | URL are tested against the routes in the order the routes appear in the DOM. Once a match is found route matching is stopped. If part of the url has still to be matched then child routes are matched against the remainder url. The process continues through subsequent child routes until the url is consumed. 12 |

13 |
Route composition
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 54 | 55 |
AttributeDescriptionExample
pathRequired - The path that will activate this route if matched against the url. TBD data paramspath="/users/:userId/details"
fullmatchOptional - This route must fully match the url without remainder to successfully match. 28 | fullmatch
elementOptional - Specifies the custom element tag to populate the outlet with for this route.element="my-custom-element"
importOptional - Imports an absolute path to the script that contains the custom element to render. The import will be loaded which ever is sooner: i) when route is rendered ii) when the page has loadedimport="/path/to/my-custom-element.js"
lazyloadOptional - Instead of loading the import after page load, it will only load te import whe it is required for the first time (when user clicks a link that requires the custom element). 43 | lazyload="true"
redirectOptional - This route will perform a redirect. 48 | redirect="/users"
disablecacheOptional - If one URL matches a route and then another identical URL matches the same route the route contents are not reevaluated. Adding the disablecache will force the route to be evaluated and rend even if the same URL is macthed. 53 | disablecache
56 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 68 | 80 | 81 | 82 | 83 | 84 | 85 | 105 | 106 | 107 | 108 | 109 | 110 | 122 | 123 | 124 | 125 | 126 | 127 | 139 | 140 | 141 | 142 | 143 | 144 | 156 | 157 | 158 | 159 | 160 | 161 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 |
Event NameDescriptionpreventDefaultevent.detail 63 |
onRouteLeaveEvent that can be cancelled to prevent navigation away from a route. Can be used as a guard.Stops the routing from taking place 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
NameValue
routeThe new RouteElement being navigated to
79 |
onRouteMatchWill prevent the match for this route. Preventing the match is just like the route not matching in the first place. The router will continue trying to match against other routes. 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
NameValue
routeThe RouteElement that made the match
matchThe match object. Ant modifications made to the match will take effect. e.g. if you add the redirect property to the match then a redirect will occur.
pathThe RouteElement path attribute value that had th esuccessful match.
104 |
onRouteNotHandledFire when a url is handled due to it not being the same origin or base urlNo effect 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
NameValue
hrefThe href that is not being handled due to it not being the same origin or base url
121 |
onRouteCancelledFires when a routing process was cancelledNo effect 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 |
NameValue
shortUrlThe url being matched sans base url
138 |
onLinkActiveStatusUpdatedFires when HTMLAnchorElement active statuses are being updated as part of a routingNo effect 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
NameValue
linksArray of all of the HTMLAnchorElement that are currently registered as router links
155 |
onRouterAddedFires when a router is added. Internal event used to plumb together the routers. Do not interfer with.No effect 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 |
NameValue
routerThe child RouterElement being added
172 |
onOutletUpdatedFire when an OutletElement is updated with a new route or assignment.No effect
181 | `; 182 | } 183 | } 184 | 185 | customElements.define('content-overview', ContentOverviewElement); -------------------------------------------------------------------------------- /examples/router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/router/router-example.js: -------------------------------------------------------------------------------- 1 | // import { RouterElement } from '../../build/es6-bundled/src/router.js' 2 | import '../../src/routes-outlet.js'; 3 | import '../../src/routes-link.js'; 4 | 5 | import './../shared/main-menu.js'; 6 | import './../shared/code-example.js'; 7 | 8 | import './content-overview.js'; 9 | import './content-nested.js'; 10 | import './content-named.js'; 11 | import './content-guards.js' 12 | 13 | class RouterExampleElement extends HTMLElement { 14 | 15 | connectedCallback() { 16 | if (!this._initialized) { 17 | this._initialized = true; 18 | this.render(); 19 | } 20 | } 21 | 22 | constructor(){ 23 | super(); 24 | } 25 | 26 | render() { 27 | this.innerHTML = ` 28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 | 42 | 43 | 44 | This content never shows because of the last catch all route 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | This route redirects to the Overview route. 58 | 59 |
60 |
61 |
62 | `; 63 | } 64 | } 65 | 66 | customElements.define('router-example', RouterExampleElement); -------------------------------------------------------------------------------- /examples/shared/code-example.js: -------------------------------------------------------------------------------- 1 | export class CodeExampleElement extends HTMLElement { 2 | 3 | connectedCallback() { 4 | if (!this._initialized) { 5 | this._initialized = true; 6 | this.render(); 7 | } 8 | } 9 | 10 | constructor() { 11 | super(); 12 | } 13 | 14 | render() { 15 | let html = ` 16 | 25 | `; 26 | let codeBlocks = document.querySelectorAll('[code-example]'); 27 | 28 | for(let i = 0, iLen = codeBlocks.length; i < iLen; i++) { 29 | html += ` 30 |
31 |

${codeBlocks[i].getAttribute('code-example')}

32 |
${prepareHtml(codeBlocks[i].innerHTML)}
33 |
34 | ` 35 | } 36 | 37 | this.innerHTML = html; 38 | } 39 | } 40 | 41 | function prepareHtml(htmlStr) { 42 | htmlStr = htmlStr.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); 43 | return htmlStr; 44 | } 45 | 46 | customElements.define('code-example', CodeExampleElement); -------------------------------------------------------------------------------- /examples/shared/common-styles.js: -------------------------------------------------------------------------------- 1 | export class CommonStylesElement extends HTMLElement { 2 | 3 | connectedCallback() { 4 | if (!this._initialized) { 5 | this._initialized = true; 6 | this.render(); 7 | } 8 | } 9 | 10 | constructor() { 11 | super(); 12 | } 13 | 14 | render() { 15 | this.innerHTML = ` 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | `; 29 | } 30 | } 31 | 32 | customElements.define('common-styles', CommonStylesElement); -------------------------------------------------------------------------------- /examples/shared/main-menu.js: -------------------------------------------------------------------------------- 1 | import './common-styles.js'; 2 | 3 | export class MainMenuElement extends HTMLElement { 4 | 5 | connectedCallback() { 6 | if (!this._initialized) { 7 | this._initialized = true; 8 | this.render(); 9 | } 10 | } 11 | 12 | constructor() { 13 | super(); 14 | } 15 | 16 | render() { 17 | this.innerHTML = ` 18 | 27 | `; 28 | } 29 | } 30 | 31 | customElements.define('main-menu', MainMenuElement); -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "es2020", 5 | "lib": ["esnext.array", "esnext", "es2017", "dom"], 6 | "rootDir": "./", 7 | "moduleResolution": "node", 8 | "checkJs": true 9 | }, 10 | "include": [ 11 | "src/**/*", 12 | "examples/**/*", 13 | "test/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /karma-unit.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Apr 22 2019 14:22:52 GMT-0400 (Eastern Daylight Time) 3 | 4 | module.exports = config => { 5 | config.set({ 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '', 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['jasmine'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | { 16 | pattern: 'test/unit/*.js', 17 | type: 'module', 18 | }, 19 | { 20 | pattern: 'src/*.js', 21 | type: 'module', 22 | included: false, 23 | }, 24 | { 25 | pattern: 'test/assets/*.js', 26 | type: 'module', 27 | included: false, 28 | }, 29 | ], 30 | 31 | // list of files / patterns to exclude 32 | exclude: [], 33 | 34 | client: { 35 | jasmine: { 36 | random: false, 37 | }, 38 | }, 39 | 40 | // preprocess matching files before serving them to the browser 41 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 42 | preprocessors: {}, 43 | 44 | // test results reporter to use 45 | // possible values: 'dots', 'progress' 46 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 47 | reporters: ['progress'], 48 | 49 | // web server port 50 | port: 9876, 51 | 52 | // enable / disable colors in the output (reporters and logs) 53 | colors: true, 54 | 55 | // level of logging 56 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 57 | logLevel: config.LOG_INFO, 58 | 59 | // enable / disable watching file and executing tests whenever any file changes 60 | autoWatch: true, 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['ChromeHeadless'], 65 | 66 | // Continuous Integration mode 67 | // if true, Karma captures browsers, runs the tests and exits 68 | singleRun: true, 69 | 70 | // Concurrency level 71 | // how many browser should be started simultaneous 72 | concurrency: Infinity, 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-wc-router", 3 | "version": "2.2.0", 4 | "description": "AWC Router, Simple, Declarative, Decoupled, Web Component Router, PRPL, Router, Routing, Outlet", 5 | "main": "./src/router.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "chai": "^4.2.0", 12 | "dev-lib-colscott": "^2.1.0", 13 | "express": "^4.17.1", 14 | "jasmine": "^3.4.0", 15 | "rimraf": "^3.0.2", 16 | "rollup": "^2.57.0", 17 | "typescript": "^4.4.3" 18 | }, 19 | "scripts": { 20 | "build": "rollup -c", 21 | "generateTypes": "npx tsc -p d.tsconfig.json", 22 | "removeTypes": "rimraf ./src/**/*d.ts", 23 | "prepublishOnly": "npm run generateTypes", 24 | "start": "node exampleServer", 25 | "test": "npm run test:unit", 26 | "test:unit": "node ./node_modules/karma/bin/karma start karma-unit.conf.js" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/colscott/a-wc-router.git" 31 | }, 32 | "keywords": [ 33 | "routing", 34 | "web", 35 | "component", 36 | "router", 37 | "zero dependencies", 38 | "declarative", 39 | "wc" 40 | ], 41 | "author": "colin scott", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/colscott/a-wc-router/issues" 45 | }, 46 | "homepage": "https://github.com/colscott/a-wc-router#readme" 47 | } 48 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/router.js', 3 | output: [ 4 | { 5 | file: 'dist/iife/router.js', 6 | format: 'iife', 7 | }, 8 | { 9 | file: 'dist/es/router.js', 10 | format: 'es', 11 | }, 12 | { 13 | file: 'dist/cjs/router.js', 14 | format: 'cjs', 15 | }, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/models.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | /** 3 | * @typedef {Object} RouterMatch 4 | * @property {Object} data 5 | * @property {string} redirect 6 | * @property {string} url 7 | * @property {boolean} useCache 8 | * @property {string} remainder 9 | */ 10 | 11 | /** 12 | * @typedef {Object} NamedRoutingHandler 13 | * @property {(url: string) => Promise} processNamedUrl 14 | * @property {() => string} getName 15 | * @property {(url: string) => boolean} canLeave 16 | */ 17 | -------------------------------------------------------------------------------- /src/named-routing.js: -------------------------------------------------------------------------------- 1 | // /@ts-check 2 | /** 3 | * @typedef {Object} Assignment 4 | * @property {string} name of item the assignment is targeting 5 | * @property {string} url fragment to be assigned to the item 6 | */ 7 | /** 8 | * @typedef {Object} ParseNamedOutletAssignment 9 | * @property {string} elementTag 10 | * @property {Map} data 11 | * @property {Object} options 12 | * @property {string} options.import 13 | */ 14 | 15 | /** 16 | * @typedef {Object} NamedMatch 17 | * @property {string} name of the route or outlet to assign to 18 | * @property {string} url - The assignment url that was matched and consumed 19 | * @property {string} urlEscaped - The url that was matched and consumed escaped of certain characters that will break the url on servers. 20 | * @property {boolean} cancelled - If a failed attempt at assignment was made 21 | * @property {ParseNamedOutletAssignment} namedOutlet - Any named outlet assignments found 22 | */ 23 | /** 24 | * Registry for named routers and outlets. 25 | * Simplifies nested routing by being able to target specific routers and outlets in a link. 26 | * Can act as a message bus of sorts. Named items being the handlers and assignments as the messages. 27 | */ 28 | export class NamedRouting { 29 | /** 30 | * Adds a router or outlet to the registry 31 | * @param {import('./models').NamedRoutingHandler} item to add 32 | */ 33 | static async addNamedItem(item) { 34 | const name = item.getName(); 35 | 36 | if (name) { 37 | if (NamedRouting.registry[name]) { 38 | throw Error(`Error adding named item ${name}, item with that name already registered`); 39 | } 40 | 41 | NamedRouting.registry[name] = item; 42 | 43 | const assignment = NamedRouting.getAssignment(name); 44 | 45 | if (assignment && item.canLeave(assignment.url)) { 46 | await item.processNamedUrl(assignment.url); 47 | } 48 | } 49 | } 50 | 51 | /** Removes an item by name from the registry if it exists. */ 52 | static removeNamedItem(name) { 53 | if (NamedRouting.registry[name]) { 54 | delete NamedRouting.registry[name]; 55 | } 56 | } 57 | 58 | /** Gets an item by name from the registry */ 59 | static getNamedItem(name) { 60 | return NamedRouting.registry[name]; 61 | } 62 | 63 | /** Gets an assignment from the registry */ 64 | static getAssignment(name) { 65 | return NamedRouting.assignments[name]; 66 | } 67 | 68 | /** 69 | * Add an assignment to the registry. Will override an assignment if one already exists with the same name. 70 | * @param {string} name the name of the named item to target with the assignment 71 | * @param {string} url to assign to the named item 72 | * @returns {Promise} when assignment is completed. false is returned if the assignment was cancelled for some reason. 73 | */ 74 | static async addAssignment(name, url) { 75 | const lastAssignment = NamedRouting.assignments[name]; 76 | NamedRouting.assignments[name] = { name, url }; 77 | const namedItem = NamedRouting.getNamedItem(name); 78 | if (namedItem) { 79 | if (namedItem.canLeave(url) === false) { 80 | NamedRouting.assignments[name] = lastAssignment; 81 | return false; 82 | } 83 | 84 | await namedItem.processNamedUrl(url); 85 | } 86 | return true; 87 | } 88 | 89 | /** Removes an assignment from the registry */ 90 | static removeAssignment(name) { 91 | if (NamedRouting.assignments[name]) { 92 | delete NamedRouting.assignments[name]; 93 | return true; 94 | } 95 | return false; 96 | } 97 | 98 | /** @returns {string} Serializes the current assignments into URL representation. */ 99 | static generateNamedItemsUrl() { 100 | return Object.values(NamedRouting.assignments).reduce( 101 | (url, assignment) => `${url.length ? '::' : ''}${NamedRouting.generateUrlFragment(assignment)}`, 102 | '', 103 | ); 104 | } 105 | 106 | /** Serializes an assignment for URL. */ 107 | static generateUrlFragment(assignment) { 108 | // Polymer server does not like the period in the import statement 109 | return `(${assignment.name}:${assignment.url.replace(/\./g, '_dot_')})`; 110 | } 111 | 112 | /** 113 | * Parses a URL section and tries to get a named item from it. 114 | * @param {string} url containing the assignment and the named item 115 | * @param {boolean} [suppressAdding] of the assignment and only return the match in a dry run 116 | * @returns {Promise} null if not able to parse. If we are adding the named item then the promise is resolved when item is added and any routing has taken place. 117 | */ 118 | static async parseNamedItem(url, suppressAdding) { 119 | let _url = url; 120 | if (_url[0] === '/') { 121 | _url = _url.substr(1); 122 | } 123 | 124 | if (_url[0] === '(') { 125 | _url = _url.substr(1, _url.length - 2); 126 | } 127 | 128 | const match = _url.match(/^\/?\(?([\w_-]+)\:(.*)\)?/); 129 | if (match) { 130 | // Polymer server does not like the period in the import statement 131 | const urlEscaped = match[2].replace(/_dot_/g, '.'); 132 | let cancelled = false; 133 | if (suppressAdding !== true) { 134 | if ((await NamedRouting.addAssignment(match[1], urlEscaped)) === false) { 135 | cancelled = true; 136 | } 137 | } 138 | return { 139 | name: match[1], 140 | url: match[2], 141 | urlEscaped, 142 | cancelled, 143 | namedOutlet: NamedRouting.parseNamedOutletUrl(match[2]), 144 | }; 145 | } 146 | 147 | return null; 148 | } 149 | 150 | /** 151 | * Takes a url for a named outlet assignment and parses 152 | * @param {string} url 153 | * @returns {ParseNamedOutletAssignment|null} null is returned if the url could not be parsed into a named outlet assignment 154 | */ 155 | static parseNamedOutletUrl(url) { 156 | const match = url.match(/^([/\w-]+)(\(.*?\))?(?:\:(.+))?/); 157 | if (match) { 158 | const data = new Map(); 159 | 160 | if (match[3]) { 161 | const keyValues = match[3].split('&'); 162 | for (let i = 0, iLen = keyValues.length; i < iLen; i++) { 163 | const keyValue = keyValues[i].split('='); 164 | data.set(decodeURIComponent(keyValue[0]), decodeURIComponent(keyValue[1])); 165 | } 166 | } 167 | const elementTag = match[1]; 168 | let importPath = match[2] && match[2].substr(1, match[2].length - 2); 169 | 170 | const inferredElementTag = NamedRouting.inferCustomElementTagName(elementTag); 171 | if (inferredElementTag === null) { 172 | return null; 173 | } 174 | 175 | if (!importPath) { 176 | importPath = NamedRouting.inferCustomElementImportPath(elementTag, inferredElementTag); 177 | } 178 | 179 | const options = { import: importPath }; 180 | return { 181 | elementTag: inferredElementTag, 182 | data, 183 | options, 184 | }; 185 | } 186 | return null; 187 | } 188 | 189 | /** 190 | * @param {string} importStyleTagName 191 | * @param {string} elementTag 192 | * @returns {string} the custom element import path inferred from the import style string 193 | */ 194 | static inferCustomElementImportPath(importStyleTagName, elementTag) { 195 | if (customElements.get(elementTag) !== undefined) { 196 | // tag is loaded. no need for import. 197 | return undefined; 198 | } 199 | 200 | let inferredPath = importStyleTagName; 201 | 202 | const lastForwardSlash = inferredPath.lastIndexOf('/'); 203 | if (lastForwardSlash === -1) { 204 | inferredPath = `/${inferredPath}`; 205 | } 206 | 207 | const dotIndex = inferredPath.indexOf('.'); 208 | if (dotIndex === -1) { 209 | inferredPath += '.js'; 210 | } 211 | 212 | return inferredPath; 213 | } 214 | 215 | /** 216 | * @param {string} elementTag 217 | * @returns {string} the custom element tag name inferred from import style string 218 | */ 219 | static inferCustomElementTagName(elementTag) { 220 | let inferredTagName = elementTag; 221 | 222 | // get class name from path 223 | const lastForwardSlash = inferredTagName.lastIndexOf('/'); 224 | if (lastForwardSlash > -1) { 225 | inferredTagName = inferredTagName.substring(lastForwardSlash + 1); 226 | } 227 | 228 | // get class name from file name 229 | const dotIndex = inferredTagName.indexOf('.'); 230 | if (dotIndex > -1) { 231 | inferredTagName = inferredTagName.substring(0, dotIndex - 1); 232 | } 233 | 234 | // to kebab case 235 | inferredTagName = inferredTagName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 236 | 237 | if (inferredTagName.indexOf('-') === -1) { 238 | inferredTagName = null; 239 | } 240 | 241 | return inferredTagName; 242 | } 243 | 244 | /** 245 | * Pre-fetches an import module so that it is available when the link is activated 246 | * @param {NamedMatch} namedAssignment item assignment 247 | * @returns {Promise} resolves when the import is completed 248 | */ 249 | static async prefetchNamedOutletImports(namedAssignment) { 250 | if ( 251 | namedAssignment.namedOutlet && 252 | namedAssignment.namedOutlet.options && 253 | namedAssignment.namedOutlet.options.import 254 | ) { 255 | await NamedRouting.pageReady(); 256 | await NamedRouting.importCustomElement( 257 | namedAssignment.namedOutlet.options.import, 258 | namedAssignment.namedOutlet.elementTag, 259 | ); 260 | } 261 | } 262 | 263 | /** 264 | * Imports a script for a customer element once the page has loaded 265 | * @param {string} importSrc 266 | * @param {string} tagName 267 | */ 268 | static async prefetchImport(importSrc, tagName) { 269 | await NamedRouting.pageReady(); 270 | await NamedRouting.importCustomElement(importSrc, tagName); 271 | } 272 | 273 | /** 274 | * Imports a script for a customer element 275 | * @param {string} importSrc 276 | * @param {string} tagName 277 | */ 278 | static async importCustomElement(importSrc, tagName) { 279 | if (importSrc && customElements.get(tagName) === undefined) { 280 | // @ts-ignore 281 | await import(/* webpackIgnore: true */ importSrc); 282 | } 283 | } 284 | 285 | /** 286 | * 287 | */ 288 | static pageReady() { 289 | if (!NamedRouting.pageReadyPromise) { 290 | NamedRouting.pageReadyPromise = 291 | document.readyState === 'complete' 292 | ? Promise.resolve() 293 | : new Promise((resolve, reject) => { 294 | /** handle readystatechange callback */ 295 | const callback = () => { 296 | if (document.readyState === 'complete') { 297 | document.removeEventListener('readystatechange', callback); 298 | resolve(); 299 | } 300 | }; 301 | document.addEventListener('readystatechange', callback); 302 | }); 303 | } 304 | 305 | return NamedRouting.pageReadyPromise; 306 | } 307 | 308 | /** 309 | * Called just before leaving for another route. 310 | * Fires an event 'routeOnLeave' that can be cancelled by preventing default on the event. 311 | * @fires RouteElement#onRouteLeave 312 | * @param {*} newRoute - the new route being navigated to 313 | * @returns bool - if the currently active route can be left 314 | */ 315 | static canLeave(newRoute) { 316 | /** 317 | * Event that can be cancelled to prevent this route from being navigated away from. 318 | * @event RouteElement#onRouteLeave 319 | * @type CustomEvent 320 | * @property {Object} details - The event details 321 | * @property {RouteElement} details.route - The RouteElement that performed the match. 322 | */ 323 | const canLeaveEvent = new CustomEvent('onRouteLeave', { 324 | bubbles: true, 325 | cancelable: true, 326 | composed: true, 327 | detail: { route: newRoute }, 328 | }); 329 | // @ts-ignore 330 | // This method is designed to be bound to a Custom Element instance. It located in here for general visibility. 331 | this.dispatchEvent(canLeaveEvent); 332 | return !canLeaveEvent.defaultPrevented; 333 | } 334 | } 335 | 336 | NamedRouting.pageReadyPromise = undefined; 337 | NamedRouting.registry = {}; 338 | /** @type {{[k: string]: Assignment}} */ 339 | NamedRouting.assignments = {}; 340 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import './routes-link.js'; 2 | 3 | export * from './named-routing.js'; 4 | export * from './routes-router.js'; 5 | export * from './routes-route.js'; 6 | export * from './routes-outlet.js'; 7 | -------------------------------------------------------------------------------- /src/routes-link.js: -------------------------------------------------------------------------------- 1 | import { RouterElement } from './routes-router.js'; 2 | 3 | /** */ 4 | class RouterLinkElement extends HTMLAnchorElement { 5 | /** @inheritdoc */ 6 | connectedCallback() { 7 | RouterElement.initialize(); 8 | this.register(); 9 | } 10 | 11 | /** @inheritdoc */ 12 | static get observedAttributes() { 13 | return ['href']; 14 | } 15 | 16 | /** 17 | * @inheritdoc 18 | * Listens for href attribute changing. If it does then it re-registers the link. 19 | */ 20 | attributeChangedCallback(name, oldValue, newValue) { 21 | if (name === 'href') { 22 | if (oldValue && newValue) { 23 | this.register(); 24 | } 25 | } 26 | } 27 | 28 | /** @inheritdoc */ 29 | constructor() { 30 | super(); 31 | } 32 | 33 | /** Helper to dispatch events that will signal the registering of links. */ 34 | register() { 35 | window.dispatchEvent( 36 | new CustomEvent('routerLinksAdded', { 37 | detail: { 38 | links: [this], 39 | }, 40 | }), 41 | ); 42 | } 43 | } 44 | 45 | window.customElements.define('router-link', RouterLinkElement, { extends: 'a' }); 46 | -------------------------------------------------------------------------------- /src/routes-outlet.js: -------------------------------------------------------------------------------- 1 | import { NamedRouting } from './named-routing.js'; 2 | import { RouterElement } from './routes-router.js'; 3 | import { RouteElement } from './routes-route.js'; 4 | 5 | /** */ 6 | export class OutletElement extends HTMLElement { 7 | /** Initialize */ 8 | async connectedCallback() { 9 | if (this.isConnected) { 10 | if (!this.created) { 11 | this.created = true; 12 | // var p = document.createElement('p'); 13 | // p.textContent = 'Please add your routes!'; 14 | // this.appendChild(p); 15 | 16 | await NamedRouting.addNamedItem(this); 17 | } 18 | await RouterElement.initialize(); 19 | } 20 | } 21 | 22 | /** Dispose */ 23 | disconnectedCallback() { 24 | if (this.getName()) { 25 | NamedRouting.removeNamedItem(this.getName()); 26 | } 27 | } 28 | 29 | /** Initialize */ 30 | constructor() { 31 | super(); 32 | 33 | this.canLeave = NamedRouting.canLeave.bind(this); 34 | } 35 | 36 | /** @returns {string} value of the attribute called name. Can not be changed was set. */ 37 | getName() { 38 | if (this.outletName === undefined) { 39 | this.outletName = this.getAttribute('name'); 40 | } 41 | return this.outletName; 42 | } 43 | 44 | /** 45 | * @private 46 | * @param {string} url to parse 47 | * @returns url broken into segments 48 | */ 49 | _createPathSegments(url) { 50 | return url.replace(/(^\/+|\/+$)/g, '').split('/'); 51 | } 52 | 53 | /** 54 | * Replaces the content of this outlet with the supplied new content 55 | * @fires OutletElement#onOutletUpdated 56 | * @param {string|DocumentFragment|Node} content - Content that will replace the current content of the outlet 57 | */ 58 | renderOutletContent(content) { 59 | this.innerHTML = ''; 60 | // console.info('outlet rendered: ' + this.outletName, content); 61 | 62 | if (typeof content === 'string') { 63 | this.innerHTML = content; 64 | } else { 65 | this.appendChild(content); 66 | } 67 | 68 | this.dispatchOutletUpdated(); 69 | } 70 | 71 | /** 72 | * Takes in a url that contains named outlet data and renders the outlet using the information 73 | * @param {string} url to parse and create outlet content for 74 | * @returns {Promise} that was added to the outlet as a result of processing the named url 75 | */ 76 | async processNamedUrl(url) { 77 | const details = NamedRouting.parseNamedOutletUrl(url); 78 | const options = details.options || { import: null }; 79 | let data = details.data || new Map(); 80 | 81 | if (data instanceof Map === false) { 82 | data = new Map(Object.entries(data || {})); 83 | } 84 | 85 | // If same tag name then just set the data 86 | if (this.children && this.children[0] && this.children[0].tagName.toLowerCase() === details.elementTag) { 87 | RouteElement.setData(this.children[0], data); 88 | this.dispatchOutletUpdated(); 89 | return; 90 | } 91 | 92 | await NamedRouting.importCustomElement(options.import, details.elementTag); 93 | 94 | const element = document.createElement(details.elementTag); 95 | RouteElement.setData(element, data); 96 | 97 | if (customElements.get(details.elementTag) === undefined) { 98 | console.error( 99 | `Custom Element not found: ${details.elementTag}. Are you missing an import or mis-spelled tag name?`, 100 | ); 101 | } 102 | 103 | this.renderOutletContent(element); 104 | } 105 | 106 | /** Dispatch the onOutletUpdate event */ 107 | dispatchOutletUpdated() { 108 | /** 109 | * Outlet updated event that fires after an Outlet replaces it's content. 110 | * @event OutletElement#onOutletUpdated 111 | * @type CustomEvent 112 | * @property {any} - Currently no information is passed in the event. 113 | */ 114 | this.dispatchEvent( 115 | new CustomEvent('onOutletUpdated', { 116 | bubbles: true, 117 | composed: true, 118 | detail: {}, 119 | }), 120 | ); 121 | } 122 | } 123 | 124 | window.customElements.define('a-outlet', OutletElement); 125 | window.customElements.define('an-outlet', class extends OutletElement {}); 126 | -------------------------------------------------------------------------------- /src/routes-route.js: -------------------------------------------------------------------------------- 1 | import { NamedRouting } from './named-routing.js'; 2 | 3 | /** @typedef {Map|HTMLOrSVGElement['dataset']} MatchData */ 4 | /** 5 | * @typedef {Object} Match 6 | * @property {string} url - The url that was matched and consumed by this route. The match.url and the match.remainder will together equal the URL that the route originally matched against. 7 | * @property {string} remainder - If the route performed a partial match, the remainder of the URL that was not attached is stored in this property. 8 | * @property {Map} data - Any data found and matched in the URL. 9 | * @property {?string} redirect - A URL to redirect to. 10 | * @property {boolean} useCache - Indicator as to wether the current HTML content can be reused. 11 | */ 12 | 13 | /** */ 14 | export class RouteElement extends HTMLElement { 15 | /** Initialize */ 16 | connectedCallback() { 17 | if (!this.created) { 18 | this.created = true; 19 | this.style.display = 'none'; 20 | const baseElement = document.head.querySelector('base'); 21 | this.baseUrl = baseElement && baseElement.getAttribute('href'); 22 | } 23 | 24 | if (this.isConnected) { 25 | const onRouteAdded = new CustomEvent('onRouteAdded', { 26 | bubbles: true, 27 | composed: true, 28 | detail: { 29 | route: this, 30 | }, 31 | }); 32 | 33 | this.dispatchEvent(onRouteAdded); 34 | 35 | const lazyLoad = (this.getAttribute('lazyload') || '').toLowerCase() === 'true' || this.hasAttribute('lazy-load'); 36 | 37 | if (lazyLoad === false) { 38 | const importAttr = this.getAttribute('import'); 39 | const tagName = this.getAttribute('element'); 40 | NamedRouting.prefetchImport(importAttr, tagName); 41 | } 42 | } 43 | } 44 | 45 | /** Initialize */ 46 | constructor() { 47 | super(); 48 | 49 | this.canLeave = NamedRouting.canLeave.bind(this); 50 | 51 | /** @type {string|DocumentFragment|Node} */ 52 | this.content = null; 53 | 54 | this.data = null; 55 | } 56 | 57 | /** 58 | * @private 59 | * @param {string} url to break into segments 60 | * @returns {Array} string broken into segments 61 | */ 62 | _createPathSegments(url) { 63 | return url.replace(/(^\/+|\/+$)/g, '').split('/'); 64 | } 65 | 66 | /** 67 | * Performs matching and partial matching. In order to successfully match, a RouteElement elements path attribute must match from the start of the URL. A full match would completely match the URL. A partial match would return from the start. 68 | * @fires RouteElement#onROuteMatch 69 | * @param {string} url - The url to perform matching against 70 | * @returns {Match} match - The resulting match. Null will be returned if no match was made. 71 | */ 72 | match(url) { 73 | const urlSegments = this._createPathSegments(url); 74 | 75 | const path = this.getAttribute('path'); 76 | if (!path) { 77 | console.info('route must contain a path'); 78 | throw new Error('Route has no path defined. Add a path attribute to route'); 79 | } 80 | 81 | const fullMatch = { 82 | url, 83 | remainder: '', 84 | data: new Map(), 85 | redirect: null, 86 | useCache: false, 87 | }; 88 | 89 | let match = fullMatch; 90 | 91 | if (path === '*') { 92 | match = fullMatch; 93 | } else if (path === url) { 94 | match = fullMatch; 95 | } else { 96 | const pathSegments = this._createPathSegments(path); 97 | // console.info(urlSegments, pathSegments); 98 | const data = match.data; 99 | 100 | const max = pathSegments.length; 101 | let i = 0; 102 | for (; i < max; i++) { 103 | if (pathSegments[i] && pathSegments[i].charAt(0) === ':') { 104 | // Handle bound values 105 | const paramName = pathSegments[i].replace(/(^\:|[+*?]+\S*$)/g, ''); 106 | const flags = (pathSegments[i].match(/([+*?])\S*$/) || [])[1] || ''; 107 | const oneOrMore = flags.includes('+'); 108 | const anyNumber = flags.includes('*'); 109 | const oneOrNone = flags.includes('?'); 110 | const defaultValue = oneOrNone && (pathSegments[i].match(/[+*?]+(\S+)$/) || [])[1] || ''; 111 | let value = urlSegments[i] || ''; 112 | const required = !anyNumber && !oneOrNone; 113 | if (!value && defaultValue) { 114 | value = defaultValue; 115 | } 116 | if (!value && required) { 117 | match = null; 118 | break; 119 | } 120 | data.set(paramName, decodeURIComponent(value)); 121 | if (oneOrMore || anyNumber) { 122 | data.set( 123 | paramName, 124 | urlSegments 125 | .slice(i) 126 | .map(decodeURIComponent) 127 | .join('/'), 128 | ); 129 | // increase i so that we know later that we have consumed all of the url segments when we're checking if we have a full match. 130 | i = urlSegments.length; 131 | break; 132 | } 133 | } else if (pathSegments[i] !== urlSegments[i]) { 134 | // Handle path segment 135 | match = null; 136 | break; 137 | } 138 | } 139 | 140 | // Check all required path segments were fulfilled 141 | if (match) { 142 | if (i >= urlSegments.length) { 143 | // Full match 144 | } else if (this.hasAttribute('fullmatch')) { 145 | // Partial match but needed full match 146 | match = null; 147 | } else if (i === max) { 148 | // Partial match 149 | match = match || fullMatch; 150 | match.data = data; 151 | match.url = urlSegments.slice(0, i).join('/'); 152 | match.remainder = urlSegments.slice(i).join('/'); 153 | } else { 154 | // No match 155 | match = null; 156 | } 157 | } 158 | } 159 | 160 | if (match !== null) { 161 | /** 162 | * Route Match event that fires after a route has performed successful matching. The event can be cancelled to prevent the match. 163 | * @event RouteElement#onRouteMatch 164 | * @type CustomEvent 165 | * @property {Object} details - The event details 166 | * @property {RouteElement} details.route - The RouteElement that performed the match. 167 | * @property {Match} details.match - The resulting match. Warning, modifications to the Match will take effect. 168 | * @property {string} details.path - The RouteElement path attribute value that was matched against. 169 | */ 170 | const routeMatchedEvent = new CustomEvent('onRouteMatch', { 171 | bubbles: true, 172 | cancelable: true, 173 | composed: true, 174 | detail: { route: this, match, path }, 175 | }); 176 | this.dispatchEvent(routeMatchedEvent); 177 | 178 | if (routeMatchedEvent.defaultPrevented) { 179 | match = null; 180 | } 181 | 182 | if (this.hasAttribute('redirect')) { 183 | match.redirect = this.getAttribute('redirect'); 184 | } 185 | } 186 | 187 | if (match) { 188 | const useCache = this.lastMatch && this.lastMatch.url === match.url && !this.hasAttribute('disableCache'); 189 | match.useCache = !!useCache; 190 | } 191 | 192 | this.lastMatch = match; 193 | 194 | return match; 195 | } 196 | 197 | /** Clear the last match which will reset cache state */ 198 | clearLastMatch() { 199 | this.lastMatch = null; 200 | } 201 | 202 | /** 203 | * Generates content for this route. 204 | * @param {Map} [attributes] - Object of properties that will be applied to the content. Only applies if the content was not generated form a Template. 205 | * @returns {Promise} - The resulting generated content. 206 | */ 207 | async getContent(attributes) { 208 | let { content } = this; 209 | 210 | if (!content) { 211 | const importAttr = this.getAttribute('import'); 212 | const tagName = this.getAttribute('element'); 213 | 214 | await NamedRouting.importCustomElement(importAttr, tagName); 215 | 216 | if (tagName) { 217 | // TODO support if tagName is a function that is called and will return the content 218 | // content = tagName(attributes); 219 | content = document.createElement(tagName); 220 | if (customElements.get(tagName) === undefined) { 221 | console.error(`Custom Element not found: ${tagName}. Are you missing an import or mis-spelled the tag name?`); 222 | } 223 | } 224 | 225 | const template = this.children[0]; 226 | if (template && template instanceof HTMLTemplateElement) { 227 | return template.content.cloneNode(true); 228 | } 229 | } 230 | 231 | if (this.data && content instanceof HTMLElement) { 232 | Object.entries(this.data).forEach(([name, value]) => { 233 | content[name] = value; 234 | }); 235 | } 236 | 237 | RouteElement.setData(content, this.dataset); 238 | 239 | // Set attributes last so they override any static properties with the same name 240 | if (attributes) { 241 | RouteElement.setData(content, attributes); 242 | } 243 | 244 | this.content = content; 245 | return this.content; 246 | } 247 | 248 | /** 249 | * @param {string|DocumentFragment|Node} target element to set the data on 250 | * @param {MatchData} data to set on the element 251 | */ 252 | static setData(target, data) { 253 | if (data && target instanceof Element) { 254 | /** 255 | * @param {string} key property name to set the value for 256 | * @param {unknown} value value to set 257 | */ 258 | const setProperty = (key, value) => { 259 | if (key[0] === '.') { 260 | target[key.substring(1)] = value; 261 | } else { 262 | target.setAttribute(key, value.toString()); 263 | } 264 | } 265 | 266 | if (data instanceof Map) { 267 | data.forEach(((value, key) => setProperty(key, value))); 268 | } else { 269 | Object.entries(data).forEach(([key, value]) => setProperty(key, value)); 270 | } 271 | } 272 | } 273 | } 274 | 275 | window.customElements.define('a-route', RouteElement); 276 | -------------------------------------------------------------------------------- /test/assets/test-dummy-two.js: -------------------------------------------------------------------------------- 1 | 2 | class TestDummyTwoElement extends HTMLElement { 3 | 4 | connectedCallback(){ 5 | if (!this.created) { 6 | this.created = true; 7 | var p = document.createElement('p'); 8 | var content = 'Test Element Two'; 9 | 10 | p.textContent = content; 11 | this.appendChild(p); 12 | } 13 | } 14 | 15 | constructor() { 16 | super(); 17 | } 18 | } 19 | 20 | window.customElements.define('test-dummy-two', TestDummyTwoElement); -------------------------------------------------------------------------------- /test/assets/test-dummy.js: -------------------------------------------------------------------------------- 1 | 2 | class TestDummyElement extends HTMLElement { 3 | 4 | render() { 5 | this.innerHTML = `

Test Element${this.getRequiredParam()}

`; 6 | } 7 | 8 | getRequiredParam() { 9 | if (this.hasAttribute('requiredParam')) { 10 | return ' ' + this.getAttribute('requiredParam'); 11 | } 12 | 13 | return ''; 14 | } 15 | 16 | static get observedAttributes() { return ['requiredparam']; } 17 | 18 | attributeChangedCallback(name, oldValue, newValue) { 19 | if (name == 'requiredparam') { 20 | this.render(); 21 | } 22 | } 23 | 24 | // TODO test fails because same element instance is reused but the attributes are not ebing rendered after beind dynamically changed. 25 | 26 | connectedCallback() { 27 | this.render(); 28 | } 29 | 30 | constructor() { 31 | super(); 32 | } 33 | } 34 | 35 | window.customElements.define('test-dummy', TestDummyElement); -------------------------------------------------------------------------------- /test/unit/end2end.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* globals expect,describe,afterEach,it */ 3 | import { NamedRouting } from '../../src/named-routing.js'; 4 | import { RouterElement } from '../../src/routes-router.js'; 5 | import '../../src/routes-route.js'; 6 | import '../../src/routes-outlet.js'; 7 | 8 | const testDelay = 100; 9 | /** Web Component be be used by suite of end to end tests */ 10 | class End2EndElement extends HTMLElement { 11 | /** Initialize */ 12 | connectedCallback() { 13 | if (this.isConnected) { 14 | this.render(); 15 | } 16 | } 17 | 18 | /** Set up HTML for tests */ 19 | render() { 20 | this.innerHTML = ` 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 106 | `; 107 | } 108 | } 109 | 110 | window.customElements.define('end-to-end', End2EndElement); 111 | const baseUrl = document.createElement('base'); 112 | baseUrl.setAttribute('href', '/myapp/'); 113 | document.head.appendChild(baseUrl); 114 | document.body.appendChild(document.createElement('end-to-end')); 115 | 116 | document.querySelector('#data-property').data = { 117 | propA: 'foobar', 118 | propB: 123, 119 | propC: true, 120 | propD: null, 121 | propE: undefined, 122 | propF: new Date(), 123 | }; 124 | 125 | /** 126 | * @param {HTMLAnchorElement} link 127 | * @returns resulting navigate event 128 | */ 129 | function fireNavigate(link) { 130 | const navigateEvent = new CustomEvent('navigate', { 131 | detail: { 132 | href: link, 133 | onNavigated: undefined, 134 | }, 135 | }); 136 | window.dispatchEvent(navigateEvent); 137 | return navigateEvent; 138 | } 139 | 140 | /** 141 | * @param {string|HTMLAnchorElement|{ href: string }} href url or link to click and navigate to 142 | * @param {() => void} [done] 143 | * @returns {Promise} onNavigated promise of the navigate event 144 | */ 145 | function click(href, done) { 146 | let link = href instanceof HTMLAnchorElement ? href : undefined; 147 | let removeLink = false; 148 | if (!link) { 149 | link = /** @type {HTMLAnchorElement} */ (document.createElement('A')); 150 | link.href = typeof href === 'string' ? href : href.href; 151 | document.body.appendChild(link); 152 | removeLink = true; 153 | } 154 | const navEvent = fireNavigate(link); 155 | console.info(navEvent.detail.onNavigated, link.href, link); 156 | return navEvent.detail.onNavigated.then(() => { 157 | removeLink && link.remove(); 158 | done && done(); 159 | return navEvent.detail.onNavigated; 160 | }); 161 | } 162 | 163 | /** 164 | * @param {{ href: string }} linkDetails to navigate to 165 | * @param {string|((HTMLElement) => void)} expectedTextOrHtmlContent to test is present 166 | * @param {'myoutlet1'|'outletA'|'outletB'|'outletC'} outletId of the outlet that will be updated bu the navigation 167 | * @param {() => void} [done] 168 | * @returns {Promise} 169 | */ 170 | function clickAndTest(linkDetails, expectedTextOrHtmlContent, outletId, done) { 171 | const _outletId = outletId || 'outletA'; 172 | console.group(`test: ${linkDetails.href}`); 173 | 174 | /** @type {Promise} */ 175 | let onNavigated = null; 176 | 177 | /** @param {CustomEvent} event */ 178 | function callback(event) { 179 | const target = event.target instanceof HTMLElement && event.target; 180 | document.body.removeEventListener('onOutletUpdated', callback); 181 | console.info(`callback for ${linkDetails.href}`); 182 | console.info(`outlet updated: ${target.id}-----${target.innerText}`); 183 | if (!_outletId || _outletId === target.id) { 184 | if (expectedTextOrHtmlContent instanceof Function) { 185 | expectedTextOrHtmlContent(event.target); 186 | } else { 187 | expect(target.innerHTML).toContain(expectedTextOrHtmlContent); 188 | } 189 | } 190 | } 191 | 192 | document.body.addEventListener('onOutletUpdated', callback); 193 | 194 | onNavigated = click(linkDetails, done).then(event => { 195 | document.body.removeEventListener('onOutletUpdated', callback); 196 | console.info(`call done for ${linkDetails.href}`); 197 | console.groupEnd(); 198 | }); 199 | 200 | return onNavigated; 201 | } 202 | 203 | /** 204 | * @param {{href: string}} linkDetails 205 | * @param {() => void} [done] 206 | * @returns {Promise} 207 | */ 208 | function clickAndNotHandle(linkDetails, done) { 209 | console.info(`clickAndNotHandle: ${linkDetails.href}`); 210 | const clickCallback = /** @param {Event} event */ event => { 211 | window.removeEventListener('click', clickCallback); 212 | event.preventDefault(); 213 | }; 214 | window.addEventListener('click', clickCallback, false); 215 | 216 | let notHandled = false; 217 | const notHandledCallback = /** @param {Event} event */ event => { 218 | notHandled = true; 219 | }; 220 | document.body.addEventListener('onRouteNotHandled', notHandledCallback); 221 | 222 | return click(linkDetails).then(() => { 223 | document.body.removeEventListener('onRouteNotHandled', notHandledCallback); 224 | expect(notHandled).toBe(true); 225 | console.info(`route not handled: ${linkDetails.href}`); 226 | console.info(`call done for ${linkDetails.href}`); 227 | done && done(); 228 | }); 229 | } 230 | 231 | afterEach(done => setTimeout(done, testDelay)); 232 | 233 | describe('link state', () => { 234 | it('link should be active for nested routes', async () => { 235 | const link1 = document.createElement('a'); 236 | link1.href = 'nested/webcomponent_nested'; 237 | document.body.appendChild(link1); 238 | 239 | const link2 = document.createElement('a'); 240 | link2.href = 'nested2/webcomponent_nested'; 241 | document.body.appendChild(link2); 242 | 243 | await RouterElement.registerLinks([link1, link2], 'active'); 244 | 245 | let onLinkActiveStatusUpdated = 0; 246 | // eslint-disable-next-line require-jsdoc 247 | const handler = () => { 248 | onLinkActiveStatusUpdated += 1; 249 | }; 250 | 251 | window.addEventListener('onLinkActiveStatusUpdated', handler); 252 | await click(link1); 253 | expect(link1.className).toEqual('active'); 254 | expect(link2.className).toEqual(''); 255 | console.info(`There are ${document.querySelectorAll('a').length} links`); 256 | expect(onLinkActiveStatusUpdated).toBeGreaterThan(0); 257 | link1.remove(); 258 | link2.remove(); 259 | window.removeEventListener('onLinkActiveStatusUpdated', handler); 260 | }); 261 | 262 | it('link should be active for named outlet', () => { 263 | const link1 = document.createElement('a'); 264 | document.body.appendChild(link1); 265 | link1.href = '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js))'; 266 | const link2 = document.createElement('a'); 267 | document.body.appendChild(link2); 268 | link2.href = '(myoutlet:tests-dummy(/base/test/assets/test-dummy.js))'; 269 | RouterElement.registerLinks([link1, link2], 'active'); 270 | 271 | let linkStatusesUpdate = false; 272 | // eslint-disable-next-line require-jsdoc 273 | const onLinkActiveStatusUpdatedCallback = event => { 274 | linkStatusesUpdate = true; 275 | }; 276 | window.addEventListener('onLinkActiveStatusUpdated', onLinkActiveStatusUpdatedCallback); 277 | 278 | return click({ href: '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js))' }).then(() => { 279 | window.removeEventListener('onLinkActiveStatusUpdated', onLinkActiveStatusUpdatedCallback); 280 | NamedRouting.removeAssignment('myoutlet1'); 281 | expect(linkStatusesUpdate).toBe(true); 282 | expect(link1.className).toEqual('active'); 283 | expect(link2.className).toEqual(''); 284 | link1.remove(); 285 | link2.remove(); 286 | }); 287 | }); 288 | 289 | it('link should be active for named outlet with data', () => { 290 | const link1 = document.createElement('a'); 291 | document.body.appendChild(link1); 292 | link1.href = '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js):param1=value1)'; 293 | const link2 = document.createElement('a'); 294 | document.body.appendChild(link2); 295 | link2.href = '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js)):param1=value2'; 296 | const link3 = document.createElement('a'); 297 | document.body.appendChild(link3); 298 | link3.href = '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js))'; 299 | RouterElement.registerLinks([link1, link2, link3], 'active'); 300 | 301 | let linkStatusesUpdate = false; 302 | // eslint-disable-next-line require-jsdoc 303 | const outletUpdateCallback = event => { 304 | linkStatusesUpdate = true; 305 | }; 306 | window.addEventListener('onLinkActiveStatusUpdated', outletUpdateCallback); 307 | 308 | return click({ href: '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js):param1=value1)' }).then(() => { 309 | window.removeEventListener('onLinkActiveStatusUpdated', outletUpdateCallback); 310 | NamedRouting.removeAssignment('myoutlet1'); 311 | expect(linkStatusesUpdate).toBe(true); 312 | expect(link1.className).toEqual('active'); 313 | expect(link2.className).toEqual(''); 314 | expect(link3.className).toEqual('active'); 315 | link1.remove(); 316 | link2.remove(); 317 | link3.remove(); 318 | }); 319 | }); 320 | 321 | it('link should be active for named routes', () => { 322 | const link1 = document.createElement('a'); 323 | document.body.appendChild(link1); 324 | link1.href = '(router-a-b:template2_nested)'; 325 | const link2 = document.createElement('a'); 326 | document.body.appendChild(link2); 327 | link2.href = '(router-a-b:template_nested)'; 328 | RouterElement.registerLinks([link1, link2], 'active'); 329 | return click({ href: 'nested/webcomponent_nested' }).then(() => { 330 | return clickAndTest({ href: '(router-a-b:template_nested)' }, 'Hello Nested', 'outletA').then(() => { 331 | NamedRouting.removeAssignment('router-a-b'); 332 | expect(link1.className).toEqual(''); 333 | expect(link2.className).toEqual('active'); 334 | link1.remove(); 335 | link2.remove(); 336 | }); 337 | }); 338 | }); 339 | }); 340 | 341 | describe('named outlets', () => { 342 | it('should show named outlet', () => { 343 | return click({ href: '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js))' }).then(() => { 344 | const content = document.querySelector("[name='myoutlet1']").innerHTML; 345 | expect(content).toContain('test-dummy'); 346 | }); 347 | }); 348 | 349 | it('updates for clicked links', () => { 350 | return clickAndTest( 351 | { href: '(myoutlet1:test-dummy(/base/test/assets/test-dummy.js):requiredParam=named outlet,testing)' }, 352 | 'Test Element named outlet,testing', 353 | 'myoutlet1', 354 | ); 355 | }); 356 | 357 | it('should support convention based importing', () => { 358 | return clickAndTest({ href: '(myoutlet1:/base/test/assets/test-dummy-two)' }, 'Test Element Two', 'myoutlet1'); 359 | }); 360 | }); 361 | 362 | describe('named routers', () => { 363 | it('updates for clicked links', async () => { 364 | await click({ href: 'nested/webcomponent_nested' }); 365 | await clickAndTest({ href: '(router-a-b:template_nested)' }, 'Hello Nested', 'outletB'); 366 | NamedRouting.removeAssignment('router-a-b'); 367 | }); 368 | }); 369 | 370 | describe('routes-router', () => { 371 | describe('simple flat', () => { 372 | it('template based route works', () => clickAndTest({ href: 'template' }, 'Hello Template', 'outletA')); 373 | 374 | it('web component based route works with import', () => 375 | clickAndTest({ href: 'webcomponent' }, '

Test Element

', 'outletA')); 376 | 377 | it('nomatch should hit catch all', () => clickAndTest({ href: 'nomatch' }, 'catch all - NotFound2', 'outletA')); 378 | 379 | it('404 if no match and no catch all', () => { 380 | const route = document.getElementById('catch-all'); 381 | route.setAttribute('path', 'other'); 382 | return clickAndTest({ href: 'exception' }, '404', 'outletA').then(() => { 383 | route.setAttribute('path', '*'); 384 | }); 385 | }); 386 | 387 | // TODO 388 | // it('only white listed routes should match', function (done) { 389 | // var route = document.getElementById('catch-all'); 390 | // route.setAttribute('path', 'other'); 391 | // clickAndTest( 392 | // { href: "exception" }, 393 | // (outlet) => { 394 | // expect(outlet.innerHTML).toContain('404'); 395 | // route.setAttribute('path', '*'); 396 | // }, 397 | // done); 398 | // }); 399 | 400 | it('absolute path should match', () => clickAndTest({ href: '/myapp/template' }, 'Hello Template', 'outletA')); 401 | 402 | it("shouldn't handle different base urls", () => { 403 | return clickAndNotHandle({ href: '/myotherapp/template' }); 404 | }); 405 | 406 | 407 | it('should correctly match multi level path name', async () => { 408 | await clickAndTest({ href: 'a/b' }, 'Foobar', 'outletA'); 409 | await clickAndTest({ href: 'a/c' }, 'Barfoo', 'outletA'); 410 | }); 411 | }); 412 | 413 | describe('auxiliary routing', () => { 414 | it('Should route auxiliary', async () => { 415 | const routerA = document.getElementById('router-a'); 416 | // @ts-ignore 417 | const auxiliaryRouting = document.getElementById('auxiliary-routing').content.cloneNode(true); 418 | routerA.parentNode.insertBefore(auxiliaryRouting, routerA.nextSibling); 419 | await click({ 420 | href: '(template)::main/(main_view1/10::main2_view1/99)::secondary/(sec_view1/54::secondary2_view1/43)', 421 | }).then(() => { 422 | expect(document.getElementById('router-a').innerText).toContain('Hello Template'); 423 | const routerB = document.getElementById('routerb'); 424 | expect(routerB.innerText).toContain('Main View 1 Main2 View 1'); 425 | const routerC = document.getElementById('routerc'); 426 | expect(routerC.innerText).toContain('Sec View 1 Sec2 View 1'); 427 | routerB.parentNode.removeChild(routerB); 428 | routerC.parentNode.removeChild(routerC); 429 | }); 430 | }); 431 | }); 432 | 433 | describe('nested routes', () => { 434 | it('should match nested', () => 435 | clickAndTest({ href: 'nested/webcomponent_nested' }, '

Test Element

', 'outletB')); 436 | 437 | describe('with data', () => { 438 | it('should work with nested data', () => 439 | clickAndTest({ href: 'nested/nested2/value1/webcomponent-data2/value2' }, 'Test Element value2', 'outletC')); 440 | }); 441 | }); 442 | 443 | describe('data', () => { 444 | it('should require data', () => 445 | clickAndTest( 446 | { href: 'webcomponent-data1/paramValue1' }, 447 | '

Test Element paramValue1

', 448 | 'outletA', 449 | )); 450 | 451 | it('supports optional data', () => 452 | clickAndTest( 453 | { href: 'webcomponent-data2' }, 454 | '

Test Element

', 455 | 'outletA', 456 | )); 457 | 458 | it('supports one or more data', () => 459 | clickAndTest( 460 | { href: 'webcomponent-data3/paramValue1' }, 461 | '

Test Element

', 462 | 'outletA', 463 | )); 464 | 465 | it('supports zero or more data', () => 466 | clickAndTest( 467 | { href: 'webcomponent-data4/paramValue1' }, 468 | '

Test Element

', 469 | 'outletA', 470 | )); 471 | 472 | it('supports multiple data params', () => 473 | clickAndTest( 474 | { href: 'webcomponent-data5/paramValue1/paramVlaue2/paramValue3' }, 475 | '

Test Element

', 476 | 'outletA', 477 | )); 478 | 479 | it('Supports setting data properties', () => 480 | clickAndTest( 481 | { href: 'webcomponent-data5/paramValue1/paramVlaue2/paramValue3' }, 482 | outlet => { 483 | const testDummy = outlet.querySelector('test-dummy'); 484 | expect(testDummy.thirdparam).toEqual('paramValue3'); 485 | expect(testDummy.getAttribute('firstParam')).toEqual('paramValue1'); 486 | expect(testDummy.getAttribute('secondParam')).toEqual('paramVlaue2'); 487 | }, 488 | 'outletA', 489 | )); 490 | 491 | it('Supports setting dataset attributes', () => 492 | clickAndTest( 493 | { href: 'webcomponent-data6' }, 494 | outlet => { 495 | const testDummy = outlet.querySelector('test-dummy'); 496 | expect(testDummy.getAttribute('firstParam')).toEqual('foobar'); 497 | expect(testDummy.getAttribute('secondParam')).toEqual('foo'); 498 | expect(testDummy.getAttribute('thirdParam')).toEqual('bar'); 499 | }, 500 | 'outletA', 501 | )); 502 | 503 | it('Supports setting data properties', () => 504 | clickAndTest( 505 | { href: 'webcomponent-data7' }, 506 | outlet => { 507 | const testDummy = outlet.querySelector('test-dummy'); 508 | expect(testDummy.getAttribute('firstParam')).toEqual('foobar'); 509 | expect(testDummy.propA).toBe('foobar'); 510 | expect(testDummy.propB).toBe(123); 511 | expect(testDummy.propC).toBeTrue(); 512 | expect(testDummy.propD).toBeNull(); 513 | expect(testDummy.propE).toBeUndefined(); 514 | expect(testDummy.propF).toBeInstanceOf(Date); 515 | }, 516 | 'outletA', 517 | )); 518 | }); 519 | 520 | describe('Route Caching', () => { 521 | it('should use cache if url is same', async () => { 522 | await click({ href: 'nested/nested2/value1/webcomponent-data2/value3' }); 523 | document.getElementById('outletC').setAttribute('test', '123'); 524 | await clickAndTest( 525 | { href: 'nested/nested2/value1/webcomponent-data2/value2' }, 526 | outlet => { 527 | expect(outlet.getAttribute('test')).toEqual('123'); 528 | }, 529 | 'outletC', 530 | ); 531 | }); 532 | 533 | it('should not use cache if url is different', async () => { 534 | await click({ href: 'nested/nested2/value2/webcomponent-data2/value3' }); 535 | const outletCBefore = document.getElementById('outletC'); 536 | outletCBefore.setAttribute('test', '123'); 537 | await click({ href: 'nested/nested2/value1/webcomponent-data2/value2' }); 538 | const outletCAfter = document.getElementById('outletC'); 539 | expect(outletCAfter).not.toEqual(outletCBefore); 540 | expect(outletCAfter.getAttribute('test')).not.toEqual('123'); 541 | }); 542 | }); 543 | 544 | describe('Route Guards', () => { 545 | it('will not leave', done => { 546 | // eslint-disable-next-line require-jsdoc 547 | const check = () => { 548 | // eslint-disable-next-line require-jsdoc 549 | const routeLeaveCallback = event => { 550 | document.body.removeEventListener('onRouteLeave', routeLeaveCallback); 551 | event.preventDefault(); 552 | // eslint-disable-next-line require-jsdoc 553 | const routeCancelledCallback = _ => { 554 | document.body.removeEventListener('onRouteCancelled', routeCancelledCallback); 555 | console.groupEnd(); 556 | }; 557 | document.body.addEventListener('onRouteCancelled', routeCancelledCallback); 558 | }; 559 | document.body.addEventListener('onRouteLeave', routeLeaveCallback); 560 | clickAndTest({ href: 'nested/nested2/value2/webcomponent-data2/value2' }, outlet => {}, 'outletC').then(() => 561 | done(), 562 | ); 563 | }; 564 | clickAndTest( 565 | { href: 'nested/nested2/value1/webcomponent-data2/value3' }, 566 | outlet => { 567 | outlet.setAttribute('test', '123'); 568 | }, 569 | 'outletC', 570 | ).then(() => check()); 571 | }); 572 | }); 573 | 574 | describe('Router Events', () => { 575 | // it('should fire onRouterAdded', done => { 576 | // }); 577 | // it('should fire onRouteCancelled', done => { 578 | // }); 579 | }); 580 | 581 | describe('Route Events', () => { 582 | // it('should fire onRouteLeave', done => { 583 | // clickAndTest({ href: 'nested/template_nested' }, 'Hello Nested', done); 584 | // }); 585 | 586 | it('should cancel match with onRouteMatch', async () => { 587 | const templateRoute = document.getElementById('template-route'); 588 | // eslint-disable-next-line require-jsdoc 589 | const routeMatchedCallback = event => { 590 | templateRoute.removeEventListener('onRouteMatch', routeMatchedCallback); 591 | event.preventDefault(); 592 | }; 593 | templateRoute.addEventListener('onRouteMatch', routeMatchedCallback); 594 | await clickAndTest({ href: 'template' }, 'Only hit if template route cancelled', 'outletA'); 595 | }); 596 | }); 597 | 598 | // describe('Outlet Events', () => { 599 | // it('should match nested', done => { 600 | // clickAndTest({ href: 'nested/template_nested' }, 'Hello Nested', done); 601 | // }); 602 | // }); 603 | 604 | // describe('test hash routing', () => { 605 | // it('should match', done => { 606 | // clickAndTest({ href: 'nested/template_nested' }, 'Hello Nested'); 607 | // }); 608 | // }); 609 | }); 610 | 611 | describe('routes-route', () => { 612 | describe('route matching', () => { 613 | // it('full matches', function () { 614 | // }); 615 | }); 616 | 617 | it('should apply dataset properties' , () => { 618 | 619 | }); 620 | }); 621 | -------------------------------------------------------------------------------- /test/unit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Tests 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/unit/routes-route.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* globals expect,describe,afterEach,it */ 3 | import { RouteElement } from '../../src/routes-route.js'; 4 | 5 | /** @returns {RouteElement} */ 6 | function createRouteElement() { 7 | return /** @type {RouteElement} */(document.createElement('a-route')); 8 | } 9 | 10 | describe('routes-route', () => { 11 | it('match multi level pathname', async () => { 12 | const routeElement = createRouteElement(); 13 | routeElement.setAttribute('path', '/a/b/:t?'); 14 | expect(routeElement.match('/a/b').url).toBe('/a/b'); 15 | expect(routeElement.match('/a/b/1').url).toBe('/a/b/1'); 16 | expect(routeElement.match('/a/c')).toBeNull(); 17 | expect(routeElement.match('/a/b/1/e/f').url).toBe('a/b/1'); 18 | expect(routeElement.match('/a/b/1/e/f').remainder).toBe('e/f'); 19 | }); 20 | 21 | it('optional parameters', () => { 22 | const routeElement = createRouteElement(); 23 | 24 | routeElement.setAttribute('path', '/a/b/:t?'); 25 | expect(routeElement.match('/a/b').url).toBe('/a/b'); 26 | expect(routeElement.match('/a/b/1').url).toBe('/a/b/1'); 27 | 28 | routeElement.setAttribute('path', '/a/b/:t?/c/:v?'); 29 | 30 | let result = routeElement.match('/a/b/3/c/4'); 31 | expect(result.url).toBe('/a/b/3/c/4'); 32 | expect(result.data).toEqual(new Map([['t', '3'], ['v', '4']])); 33 | 34 | result = routeElement.match('/a/b/3/c'); 35 | expect(result.url).toBe('/a/b/3/c'); 36 | expect(result.data).toEqual(new Map([['t', '3'], ['v', '']])); 37 | expect(routeElement.match('/a/b/c')).toBeNull(); 38 | 39 | result = routeElement.match('/a/b/3/c/4/d/5'); 40 | expect(result.url).toBe('a/b/3/c/4'); 41 | expect(result.remainder).toBe('d/5'); 42 | expect(result.data).toEqual(new Map([['t', '3'], ['v', '4']])); 43 | 44 | routeElement.setAttribute('fullMatch', ''); 45 | result = routeElement.match('/a/b/3/c/4/d/5'); 46 | expect(result).toBeNull(); 47 | }); 48 | 49 | it('required parameters', () => { 50 | const routeElement = createRouteElement(); 51 | 52 | routeElement.setAttribute('path', '/a/b/:t'); 53 | expect(routeElement.match('/a/b')).toBeNull(); 54 | expect(routeElement.match('/a/b/1').url).toBe('/a/b/1'); 55 | expect(routeElement.match('/a/b/1').data).toEqual(new Map([['t', '1']])); 56 | 57 | routeElement.setAttribute('path', '/a/b/:t/c/:v'); 58 | 59 | let result = routeElement.match('/a/b/3/c/4'); 60 | expect(result.url).toBe('/a/b/3/c/4'); 61 | expect(result.data).toEqual(new Map([['t', '3'], ['v', '4']])); 62 | 63 | result = routeElement.match('/a/b/3/c'); 64 | expect(result).toBeNull(); 65 | expect(routeElement.match('/a/b/c')).toBeNull(); 66 | 67 | result = routeElement.match('/a/b/3/c/4/d/5'); 68 | expect(result.url).toBe('a/b/3/c/4'); 69 | expect(result.remainder).toBe('d/5'); 70 | expect(result.data).toEqual(new Map([['t', '3'], ['v', '4']])); 71 | }); 72 | 73 | it('one or more parameters', () => { 74 | const routeElement = createRouteElement(); 75 | 76 | routeElement.setAttribute('path', '/a/b/:t+'); 77 | expect(routeElement.match('/a/b')).toBeNull(); 78 | expect(routeElement.match('/a/b/1/2/3').url).toBe('/a/b/1/2/3'); 79 | expect(routeElement.match('/a/b/1/2/3').data).toEqual(new Map([['t', '1/2/3']])); 80 | }); 81 | 82 | it('any number of parameters', () => { 83 | const routeElement = createRouteElement(); 84 | 85 | routeElement.setAttribute('path', '/a/b/:t*'); 86 | let match = routeElement.match('/a/b'); 87 | expect(match.url).toBe('/a/b'); 88 | expect(match.data).toEqual(new Map([['t', '']])); 89 | match = routeElement.match('/a/b/1/2/3'); 90 | expect(match.url).toBe('/a/b/1/2/3'); 91 | expect(match.data).toEqual(new Map([['t', '1/2/3']])); 92 | }); 93 | 94 | it('default parameter values', () => { 95 | const routeElement = createRouteElement(); 96 | 97 | routeElement.setAttribute('path', '/a/b/:t?foobar'); 98 | let match = routeElement.match('/a/b'); 99 | expect(match.url).toBe('/a/b'); 100 | expect(match.data).toEqual(new Map([['t', 'foobar']])); 101 | match = routeElement.match('/a/b/1'); 102 | expect(match.url).toBe('/a/b/1'); 103 | expect(match.data).toEqual(new Map([['t', '1']])); 104 | }); 105 | }); 106 | --------------------------------------------------------------------------------