| 1 | // GENERATED by tools/embed.sh from shared/ and static/. Do not edit. |
| 2 | |
| 3 | enum Assets { |
| 4 | static let baseCSS = #""" |
| 5 | hr { |
| 6 | margin: 20px 0; |
| 7 | border: 0; |
| 8 | border-top: 1px dashed #c5c5c5; |
| 9 | border-bottom: 1px dashed #f7f7f7; |
| 10 | } |
| 11 | |
| 12 | .learn a { |
| 13 | font-weight: normal; |
| 14 | text-decoration: none; |
| 15 | color: #b83f45; |
| 16 | } |
| 17 | |
| 18 | .learn a:hover { |
| 19 | text-decoration: underline; |
| 20 | color: #787e7e; |
| 21 | } |
| 22 | |
| 23 | .learn h3, |
| 24 | .learn h4, |
| 25 | .learn h5 { |
| 26 | margin: 10px 0; |
| 27 | font-weight: 500; |
| 28 | line-height: 1.2; |
| 29 | color: #000; |
| 30 | } |
| 31 | |
| 32 | .learn h3 { |
| 33 | font-size: 24px; |
| 34 | } |
| 35 | |
| 36 | .learn h4 { |
| 37 | font-size: 18px; |
| 38 | } |
| 39 | |
| 40 | .learn h5 { |
| 41 | margin-bottom: 0; |
| 42 | font-size: 14px; |
| 43 | } |
| 44 | |
| 45 | .learn ul { |
| 46 | padding: 0; |
| 47 | margin: 0 0 30px 25px; |
| 48 | } |
| 49 | |
| 50 | .learn li { |
| 51 | line-height: 20px; |
| 52 | } |
| 53 | |
| 54 | .learn p { |
| 55 | font-size: 15px; |
| 56 | font-weight: 300; |
| 57 | line-height: 1.3; |
| 58 | margin-top: 0; |
| 59 | margin-bottom: 0; |
| 60 | } |
| 61 | |
| 62 | #issue-count { |
| 63 | display: none; |
| 64 | } |
| 65 | |
| 66 | .quote { |
| 67 | border: none; |
| 68 | margin: 20px 0 60px 0; |
| 69 | } |
| 70 | |
| 71 | .quote p { |
| 72 | font-style: italic; |
| 73 | } |
| 74 | |
| 75 | .quote p:before { |
| 76 | content: '“'; |
| 77 | font-size: 50px; |
| 78 | opacity: .15; |
| 79 | position: absolute; |
| 80 | top: -20px; |
| 81 | left: 3px; |
| 82 | } |
| 83 | |
| 84 | .quote p:after { |
| 85 | content: '”'; |
| 86 | font-size: 50px; |
| 87 | opacity: .15; |
| 88 | position: absolute; |
| 89 | bottom: -42px; |
| 90 | right: 3px; |
| 91 | } |
| 92 | |
| 93 | .quote footer { |
| 94 | position: absolute; |
| 95 | bottom: -40px; |
| 96 | right: 0; |
| 97 | } |
| 98 | |
| 99 | .quote footer img { |
| 100 | border-radius: 3px; |
| 101 | } |
| 102 | |
| 103 | .quote footer a { |
| 104 | margin-left: 5px; |
| 105 | vertical-align: middle; |
| 106 | } |
| 107 | |
| 108 | .speech-bubble { |
| 109 | position: relative; |
| 110 | padding: 10px; |
| 111 | background: rgba(0, 0, 0, .04); |
| 112 | border-radius: 5px; |
| 113 | } |
| 114 | |
| 115 | .speech-bubble:after { |
| 116 | content: ''; |
| 117 | position: absolute; |
| 118 | top: 100%; |
| 119 | right: 30px; |
| 120 | border: 13px solid transparent; |
| 121 | border-top-color: rgba(0, 0, 0, .04); |
| 122 | } |
| 123 | |
| 124 | .learn-bar > .learn { |
| 125 | position: absolute; |
| 126 | width: 272px; |
| 127 | top: 8px; |
| 128 | left: -300px; |
| 129 | padding: 10px; |
| 130 | border-radius: 5px; |
| 131 | background-color: rgba(255, 255, 255, .6); |
| 132 | transition-property: left; |
| 133 | transition-duration: 500ms; |
| 134 | } |
| 135 | |
| 136 | @media (min-width: 899px) { |
| 137 | .learn-bar { |
| 138 | width: auto; |
| 139 | padding-left: 300px; |
| 140 | } |
| 141 | |
| 142 | .learn-bar > .learn { |
| 143 | left: 8px; |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | """# |
| 148 | |
| 149 | static let indexCSS = #""" |
| 150 | @charset 'utf-8'; |
| 151 | |
| 152 | html, |
| 153 | body { |
| 154 | margin: 0; |
| 155 | padding: 0; |
| 156 | } |
| 157 | |
| 158 | button { |
| 159 | margin: 0; |
| 160 | padding: 0; |
| 161 | border: 0; |
| 162 | background: none; |
| 163 | font-size: 100%; |
| 164 | vertical-align: baseline; |
| 165 | font-family: inherit; |
| 166 | font-weight: inherit; |
| 167 | color: inherit; |
| 168 | -webkit-appearance: none; |
| 169 | appearance: none; |
| 170 | -webkit-font-smoothing: antialiased; |
| 171 | -moz-osx-font-smoothing: grayscale; |
| 172 | } |
| 173 | |
| 174 | body { |
| 175 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; |
| 176 | line-height: 1.4em; |
| 177 | background: #f5f5f5; |
| 178 | color: #111111; |
| 179 | min-width: 230px; |
| 180 | max-width: 550px; |
| 181 | margin: 0 auto; |
| 182 | -webkit-font-smoothing: antialiased; |
| 183 | -moz-osx-font-smoothing: grayscale; |
| 184 | font-weight: 300; |
| 185 | } |
| 186 | |
| 187 | .hidden { |
| 188 | display: none; |
| 189 | } |
| 190 | |
| 191 | .todoapp { |
| 192 | background: #fff; |
| 193 | margin: 130px 0 40px 0; |
| 194 | position: relative; |
| 195 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), |
| 196 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); |
| 197 | } |
| 198 | |
| 199 | .todoapp input::-webkit-input-placeholder { |
| 200 | font-style: italic; |
| 201 | font-weight: 400; |
| 202 | color: rgba(0, 0, 0, 0.4); |
| 203 | } |
| 204 | |
| 205 | .todoapp input::-moz-placeholder { |
| 206 | font-style: italic; |
| 207 | font-weight: 400; |
| 208 | color: rgba(0, 0, 0, 0.4); |
| 209 | } |
| 210 | |
| 211 | .todoapp input::input-placeholder { |
| 212 | font-style: italic; |
| 213 | font-weight: 400; |
| 214 | color: rgba(0, 0, 0, 0.4); |
| 215 | } |
| 216 | |
| 217 | .todoapp h1 { |
| 218 | position: absolute; |
| 219 | top: -140px; |
| 220 | width: 100%; |
| 221 | font-size: 80px; |
| 222 | font-weight: 200; |
| 223 | text-align: center; |
| 224 | color: #b83f45; |
| 225 | -webkit-text-rendering: optimizeLegibility; |
| 226 | -moz-text-rendering: optimizeLegibility; |
| 227 | text-rendering: optimizeLegibility; |
| 228 | } |
| 229 | |
| 230 | .new-todo, |
| 231 | .edit { |
| 232 | position: relative; |
| 233 | margin: 0; |
| 234 | width: 100%; |
| 235 | font-size: 24px; |
| 236 | font-family: inherit; |
| 237 | font-weight: inherit; |
| 238 | line-height: 1.4em; |
| 239 | color: inherit; |
| 240 | padding: 6px; |
| 241 | border: 1px solid #999; |
| 242 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); |
| 243 | box-sizing: border-box; |
| 244 | -webkit-font-smoothing: antialiased; |
| 245 | -moz-osx-font-smoothing: grayscale; |
| 246 | } |
| 247 | |
| 248 | .new-todo { |
| 249 | padding: 16px 16px 16px 60px; |
| 250 | height: 65px; |
| 251 | border: none; |
| 252 | background: rgba(0, 0, 0, 0.003); |
| 253 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); |
| 254 | } |
| 255 | |
| 256 | .main { |
| 257 | position: relative; |
| 258 | z-index: 2; |
| 259 | border-top: 1px solid #e6e6e6; |
| 260 | } |
| 261 | |
| 262 | .toggle-all { |
| 263 | width: 1px; |
| 264 | height: 1px; |
| 265 | border: none; /* Mobile Safari */ |
| 266 | opacity: 0; |
| 267 | position: absolute; |
| 268 | right: 100%; |
| 269 | bottom: 100%; |
| 270 | } |
| 271 | |
| 272 | .toggle-all + label { |
| 273 | display: flex; |
| 274 | align-items: center; |
| 275 | justify-content: center; |
| 276 | width: 45px; |
| 277 | height: 65px; |
| 278 | font-size: 0; |
| 279 | position: absolute; |
| 280 | top: -65px; |
| 281 | left: -0; |
| 282 | } |
| 283 | |
| 284 | .toggle-all + label:before { |
| 285 | content: '❯'; |
| 286 | display: inline-block; |
| 287 | font-size: 22px; |
| 288 | color: #949494; |
| 289 | padding: 10px 27px 10px 27px; |
| 290 | -webkit-transform: rotate(90deg); |
| 291 | transform: rotate(90deg); |
| 292 | } |
| 293 | |
| 294 | .toggle-all:checked + label:before { |
| 295 | color: #484848; |
| 296 | } |
| 297 | |
| 298 | .todo-list { |
| 299 | margin: 0; |
| 300 | padding: 0; |
| 301 | list-style: none; |
| 302 | } |
| 303 | |
| 304 | .todo-list li { |
| 305 | position: relative; |
| 306 | font-size: 24px; |
| 307 | border-bottom: 1px solid #ededed; |
| 308 | } |
| 309 | |
| 310 | .todo-list li:last-child { |
| 311 | border-bottom: none; |
| 312 | } |
| 313 | |
| 314 | .todo-list li.editing { |
| 315 | border-bottom: none; |
| 316 | padding: 0; |
| 317 | } |
| 318 | |
| 319 | .todo-list li.editing .edit { |
| 320 | display: block; |
| 321 | width: calc(100% - 43px); |
| 322 | padding: 12px 16px; |
| 323 | margin: 0 0 0 43px; |
| 324 | } |
| 325 | |
| 326 | .todo-list li.editing .view { |
| 327 | display: none; |
| 328 | } |
| 329 | |
| 330 | .todo-list li .toggle { |
| 331 | text-align: center; |
| 332 | width: 40px; |
| 333 | /* auto, since non-WebKit browsers doesn't support input styling */ |
| 334 | height: auto; |
| 335 | position: absolute; |
| 336 | top: 0; |
| 337 | bottom: 0; |
| 338 | margin: auto 0; |
| 339 | border: none; /* Mobile Safari */ |
| 340 | -webkit-appearance: none; |
| 341 | appearance: none; |
| 342 | } |
| 343 | |
| 344 | .todo-list li .toggle { |
| 345 | opacity: 0; |
| 346 | } |
| 347 | |
| 348 | .todo-list li .toggle + label { |
| 349 | /* |
| 350 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 |
| 351 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ |
| 352 | */ |
| 353 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); |
| 354 | background-repeat: no-repeat; |
| 355 | background-position: center left; |
| 356 | } |
| 357 | |
| 358 | .todo-list li .toggle:checked + label { |
| 359 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); |
| 360 | } |
| 361 | |
| 362 | .todo-list li label { |
| 363 | overflow-wrap: break-word; |
| 364 | padding: 15px 15px 15px 60px; |
| 365 | display: block; |
| 366 | line-height: 1.2; |
| 367 | transition: color 0.4s; |
| 368 | font-weight: 400; |
| 369 | color: #484848; |
| 370 | } |
| 371 | |
| 372 | .todo-list li.completed label { |
| 373 | color: #949494; |
| 374 | text-decoration: line-through; |
| 375 | } |
| 376 | |
| 377 | .todo-list li .destroy { |
| 378 | display: none; |
| 379 | position: absolute; |
| 380 | top: 0; |
| 381 | right: 10px; |
| 382 | bottom: 0; |
| 383 | width: 40px; |
| 384 | height: 40px; |
| 385 | margin: auto 0; |
| 386 | font-size: 30px; |
| 387 | color: #949494; |
| 388 | transition: color 0.2s ease-out; |
| 389 | } |
| 390 | |
| 391 | .todo-list li .destroy:hover, |
| 392 | .todo-list li .destroy:focus { |
| 393 | color: #C18585; |
| 394 | } |
| 395 | |
| 396 | .todo-list li .destroy:after { |
| 397 | content: '×'; |
| 398 | display: block; |
| 399 | height: 100%; |
| 400 | line-height: 1.1; |
| 401 | } |
| 402 | |
| 403 | .todo-list li:hover .destroy { |
| 404 | display: block; |
| 405 | } |
| 406 | |
| 407 | .todo-list li .edit { |
| 408 | display: none; |
| 409 | } |
| 410 | |
| 411 | .todo-list li.editing:last-child { |
| 412 | margin-bottom: -1px; |
| 413 | } |
| 414 | |
| 415 | .footer { |
| 416 | padding: 10px 15px; |
| 417 | height: 20px; |
| 418 | text-align: center; |
| 419 | font-size: 15px; |
| 420 | border-top: 1px solid #e6e6e6; |
| 421 | } |
| 422 | |
| 423 | .footer:before { |
| 424 | content: ''; |
| 425 | position: absolute; |
| 426 | right: 0; |
| 427 | bottom: 0; |
| 428 | left: 0; |
| 429 | height: 50px; |
| 430 | overflow: hidden; |
| 431 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), |
| 432 | 0 8px 0 -3px #f6f6f6, |
| 433 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), |
| 434 | 0 16px 0 -6px #f6f6f6, |
| 435 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); |
| 436 | } |
| 437 | |
| 438 | .todo-count { |
| 439 | float: left; |
| 440 | text-align: left; |
| 441 | } |
| 442 | |
| 443 | .todo-count strong { |
| 444 | font-weight: 300; |
| 445 | } |
| 446 | |
| 447 | .filters { |
| 448 | margin: 0; |
| 449 | padding: 0; |
| 450 | list-style: none; |
| 451 | position: absolute; |
| 452 | right: 0; |
| 453 | left: 0; |
| 454 | } |
| 455 | |
| 456 | .filters li { |
| 457 | display: inline; |
| 458 | } |
| 459 | |
| 460 | .filters li a { |
| 461 | color: inherit; |
| 462 | margin: 3px; |
| 463 | padding: 3px 7px; |
| 464 | text-decoration: none; |
| 465 | border: 1px solid transparent; |
| 466 | border-radius: 3px; |
| 467 | } |
| 468 | |
| 469 | .filters li a:hover { |
| 470 | border-color: #DB7676; |
| 471 | } |
| 472 | |
| 473 | .filters li a.selected { |
| 474 | border-color: #CE4646; |
| 475 | } |
| 476 | |
| 477 | .clear-completed, |
| 478 | html .clear-completed:active { |
| 479 | float: right; |
| 480 | position: relative; |
| 481 | line-height: 19px; |
| 482 | text-decoration: none; |
| 483 | cursor: pointer; |
| 484 | } |
| 485 | |
| 486 | .clear-completed:hover { |
| 487 | text-decoration: underline; |
| 488 | } |
| 489 | |
| 490 | .info { |
| 491 | margin: 65px auto 0; |
| 492 | color: #4d4d4d; |
| 493 | font-size: 11px; |
| 494 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); |
| 495 | text-align: center; |
| 496 | } |
| 497 | |
| 498 | .info p { |
| 499 | line-height: 1; |
| 500 | } |
| 501 | |
| 502 | .info a { |
| 503 | color: inherit; |
| 504 | text-decoration: none; |
| 505 | font-weight: 400; |
| 506 | } |
| 507 | |
| 508 | .info a:hover { |
| 509 | text-decoration: underline; |
| 510 | } |
| 511 | |
| 512 | /* |
| 513 | Hack to remove background from Mobile Safari. |
| 514 | Can't use it globally since it destroys checkboxes in Firefox |
| 515 | */ |
| 516 | @media screen and (-webkit-min-device-pixel-ratio:0) { |
| 517 | .toggle-all, |
| 518 | .todo-list li .toggle { |
| 519 | background: none; |
| 520 | } |
| 521 | |
| 522 | .todo-list li .toggle { |
| 523 | height: 40px; |
| 524 | } |
| 525 | } |
| 526 | |
| 527 | @media (max-width: 430px) { |
| 528 | .footer { |
| 529 | height: 50px; |
| 530 | } |
| 531 | |
| 532 | .filters { |
| 533 | bottom: 10px; |
| 534 | } |
| 535 | } |
| 536 | |
| 537 | :focus, |
| 538 | .toggle:focus + label, |
| 539 | .toggle-all:focus + label { |
| 540 | box-shadow: 0 0 2px 2px #CF7D7D; |
| 541 | outline: 0; |
| 542 | } |
| 543 | |
| 544 | """# |
| 545 | |
| 546 | static let appCSS = #""" |
| 547 | /* The controls a page without JavaScript uses. app.js's inline starter puts |
| 548 | the class "js" on <html>, and then they're hidden: the checkboxes, |
| 549 | double-click and blur take over, exactly as in TodoMVC. */ |
| 550 | .js .nojs { |
| 551 | display: none !important; |
| 552 | } |
| 553 | |
| 554 | /* Forms that only wrap a control take no part in layout. */ |
| 555 | form.inline { |
| 556 | display: contents; |
| 557 | } |
| 558 | |
| 559 | /* Without JavaScript, a submit button lies exactly over each checkbox, so |
| 560 | the page looks the same either way. */ |
| 561 | .todo-list li .view > button[data-action="toggle"] { |
| 562 | position: absolute; |
| 563 | top: 0; |
| 564 | bottom: 0; |
| 565 | left: 0; |
| 566 | width: 40px; |
| 567 | height: 40px; |
| 568 | margin: auto 0; |
| 569 | font-size: 0; |
| 570 | cursor: pointer; |
| 571 | z-index: 1; |
| 572 | } |
| 573 | |
| 574 | .main button[data-action="toggle-all"] { |
| 575 | position: absolute; |
| 576 | top: -65px; |
| 577 | left: 0; |
| 578 | width: 45px; |
| 579 | height: 65px; |
| 580 | font-size: 0; |
| 581 | cursor: pointer; |
| 582 | z-index: 1; |
| 583 | } |
| 584 | |
| 585 | /* Without JavaScript there's no double-click, so each item has an edit link |
| 586 | beside the delete button. */ |
| 587 | .todo-list li .view > a[data-action="edit"] { |
| 588 | position: absolute; |
| 589 | top: 0; |
| 590 | bottom: 0; |
| 591 | right: 55px; |
| 592 | height: 40px; |
| 593 | margin: auto 0; |
| 594 | font-size: 20px; |
| 595 | line-height: 40px; |
| 596 | color: #949494; |
| 597 | text-decoration: none; |
| 598 | } |
| 599 | |
| 600 | .todo-list li .view > a[data-action="edit"]:hover, |
| 601 | .todo-list li .view > a[data-action="edit"]:focus { |
| 602 | color: #484848; |
| 603 | } |
| 604 | |
| 605 | html:not(.js) .todo-list li label { |
| 606 | padding-right: 90px; |
| 607 | } |
| 608 | |
| 609 | """# |
| 610 | |
| 611 | static let appJS = #""" |
| 612 | // TodoMVC behaviour on top of the server-rendered page. Every change is |
| 613 | // still the page's own form post; this sends it with fetch and swaps in |
| 614 | // the new list instead of loading the page again. |
| 615 | |
| 616 | const app = document.querySelector('.todoapp'); |
| 617 | |
| 618 | // Actions run one at a time, in the order they were made, so each answer |
| 619 | // is the newest state when it's swapped in and an older one can never |
| 620 | // overwrite it. |
| 621 | let queue = Promise.resolve(); |
| 622 | |
| 623 | // Sends a form and replaces section.main and footer.footer with what the |
| 624 | // server answers. The form is read when it's submitted, so the visitor can |
| 625 | // carry on typing. Anything unexpected falls back to a full page load. |
| 626 | function send(form) { |
| 627 | const action = form.action; |
| 628 | const body = new URLSearchParams(new FormData(form)); |
| 629 | queue = queue.then(async () => { |
| 630 | const res = await fetch(action, { |
| 631 | method: 'POST', |
| 632 | headers: { 'X-Todo-Partial': '1' }, |
| 633 | body, |
| 634 | }); |
| 635 | if (!res.ok) throw new Error(`${res.status}`); |
| 636 | swap(await res.text()); |
| 637 | }).catch(() => location.reload()); |
| 638 | } |
| 639 | |
| 640 | function swap(html) { |
| 641 | const tpl = document.createElement('template'); |
| 642 | tpl.innerHTML = html; |
| 643 | app.querySelectorAll(':scope > .main, :scope > .footer').forEach((el) => el.remove()); |
| 644 | app.append(tpl.content); |
| 645 | } |
| 646 | |
| 647 | app.addEventListener('submit', (e) => { |
| 648 | e.preventDefault(); |
| 649 | const form = e.target; |
| 650 | send(form); |
| 651 | // The new-todo input is ready for the next title straight away. |
| 652 | if (form.closest('.header')) form.reset(); |
| 653 | // An edit is saved once: the blur that follows doesn't save it again. |
| 654 | const li = form.closest('.todo-list li'); |
| 655 | if (li) li.dataset.saved = '1'; |
| 656 | }); |
| 657 | |
| 658 | app.addEventListener('change', (e) => { |
| 659 | const li = e.target.closest('li[data-id]'); |
| 660 | if (e.target.matches('.toggle') && li) send(document.getElementById(`toggle-${li.dataset.id}`)); |
| 661 | if (e.target.matches('.toggle-all')) send(app.querySelector('[data-action="toggle-all"]').form); |
| 662 | }); |
| 663 | |
| 664 | // Editing: double-click opens the editor, Enter or blur saves, Escape |
| 665 | // cancels. An empty title deletes the item (the server decides that). |
| 666 | function startEditing(li) { |
| 667 | li.classList.add('editing'); |
| 668 | const input = li.querySelector('.edit'); |
| 669 | input.focus(); |
| 670 | input.setSelectionRange(input.value.length, input.value.length); |
| 671 | } |
| 672 | |
| 673 | app.addEventListener('dblclick', (e) => { |
| 674 | const li = e.target.closest('.todo-list li'); |
| 675 | if (li && e.target.matches('.view label')) startEditing(li); |
| 676 | }); |
| 677 | |
| 678 | app.addEventListener('keydown', (e) => { |
| 679 | if (e.key !== 'Escape' || !e.target.matches('.edit')) return; |
| 680 | const input = e.target; |
| 681 | input.value = input.defaultValue; |
| 682 | input.closest('li').classList.remove('editing'); |
| 683 | input.blur(); |
| 684 | }); |
| 685 | |
| 686 | app.addEventListener('focusout', (e) => { |
| 687 | if (!e.target.matches('.edit')) return; |
| 688 | const li = e.target.closest('li'); |
| 689 | if (!li.classList.contains('editing') || li.dataset.saved) return; |
| 690 | li.dataset.saved = '1'; |
| 691 | send(e.target.form); |
| 692 | }); |
| 693 | |
| 694 | // The server's edit view (?edit=<id>) opens with that item's editor; put |
| 695 | // the cursor at the end, as double-click does. |
| 696 | const editing = app.querySelector('.todo-list li.editing'); |
| 697 | if (editing) startEditing(editing); |
| 698 | |
| 699 | """# |
| 700 | |
| 701 | } |
| 702 | |