1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import m from 'mithril'; 16import {classNames} from '../../base/classnames'; 17import {Hotkey, Platform} from '../../base/hotkeys'; 18import {isString} from '../../base/object_utils'; 19import {Icons} from '../../base/semantic_icons'; 20import {Anchor} from '../../widgets/anchor'; 21import {Button} from '../../widgets/button'; 22import {Callout} from '../../widgets/callout'; 23import {Checkbox} from '../../widgets/checkbox'; 24import {Editor} from '../../widgets/editor'; 25import {EmptyState} from '../../widgets/empty_state'; 26import {Form, FormLabel} from '../../widgets/form'; 27import {HotkeyGlyphs} from '../../widgets/hotkey_glyphs'; 28import {Icon} from '../../widgets/icon'; 29import {Menu, MenuDivider, MenuItem, PopupMenu} from '../../widgets/menu'; 30import {showModal} from '../../widgets/modal'; 31import { 32 MultiSelect, 33 MultiSelectDiff, 34 PopupMultiSelect, 35} from '../../widgets/multiselect'; 36import {Popup, PopupPosition} from '../../widgets/popup'; 37import {Portal} from '../../widgets/portal'; 38import {Select} from '../../widgets/select'; 39import {Spinner} from '../../widgets/spinner'; 40import {Switch} from '../../widgets/switch'; 41import {TextInput} from '../../widgets/text_input'; 42import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph'; 43import {LazyTreeNode, Tree, TreeNode} from '../../widgets/tree'; 44import {VegaView} from '../../components/widgets/vega_view'; 45import {PageAttrs} from '../../public/page'; 46import {TableShowcase} from './table_showcase'; 47import {TreeTable, TreeTableAttrs} from '../../components/widgets/treetable'; 48import {Intent} from '../../widgets/common'; 49import { 50 VirtualTable, 51 VirtualTableAttrs, 52 VirtualTableRow, 53} from '../../widgets/virtual_table'; 54import {TagInput} from '../../widgets/tag_input'; 55import {SegmentedButtons} from '../../widgets/segmented_buttons'; 56import {MiddleEllipsis} from '../../widgets/middle_ellipsis'; 57import {Chip, ChipBar} from '../../widgets/chip'; 58import {TrackShell} from '../../widgets/track_shell'; 59import {CopyableLink} from '../../widgets/copyable_link'; 60import {VirtualOverlayCanvas} from '../../widgets/virtual_overlay_canvas'; 61import {SplitPanel} from '../../widgets/split_panel'; 62import {TabbedSplitPanel} from '../../widgets/tabbed_split_panel'; 63import {parseAndPrintTree} from '../../base/perfetto_sql_lang/language'; 64 65const DATA_ENGLISH_LETTER_FREQUENCY = { 66 table: [ 67 {category: 'a', amount: 8.167}, 68 {category: 'b', amount: 1.492}, 69 {category: 'c', amount: 2.782}, 70 {category: 'd', amount: 4.253}, 71 {category: 'e', amount: 12.7}, 72 {category: 'f', amount: 2.228}, 73 {category: 'g', amount: 2.015}, 74 {category: 'h', amount: 6.094}, 75 {category: 'i', amount: 6.966}, 76 {category: 'j', amount: 0.253}, 77 {category: 'k', amount: 1.772}, 78 {category: 'l', amount: 4.025}, 79 {category: 'm', amount: 2.406}, 80 {category: 'n', amount: 6.749}, 81 {category: 'o', amount: 7.507}, 82 {category: 'p', amount: 1.929}, 83 {category: 'q', amount: 0.095}, 84 {category: 'r', amount: 5.987}, 85 {category: 's', amount: 6.327}, 86 {category: 't', amount: 9.056}, 87 {category: 'u', amount: 2.758}, 88 {category: 'v', amount: 0.978}, 89 {category: 'w', amount: 2.36}, 90 {category: 'x', amount: 0.25}, 91 {category: 'y', amount: 1.974}, 92 {category: 'z', amount: 0.074}, 93 ], 94}; 95 96const DATA_POLISH_LETTER_FREQUENCY = { 97 table: [ 98 {category: 'a', amount: 8.965}, 99 {category: 'b', amount: 1.482}, 100 {category: 'c', amount: 3.988}, 101 {category: 'd', amount: 3.293}, 102 {category: 'e', amount: 7.921}, 103 {category: 'f', amount: 0.312}, 104 {category: 'g', amount: 1.377}, 105 {category: 'h', amount: 1.072}, 106 {category: 'i', amount: 8.286}, 107 {category: 'j', amount: 2.343}, 108 {category: 'k', amount: 3.411}, 109 {category: 'l', amount: 2.136}, 110 {category: 'm', amount: 2.911}, 111 {category: 'n', amount: 5.6}, 112 {category: 'o', amount: 7.59}, 113 {category: 'p', amount: 3.101}, 114 {category: 'q', amount: 0.003}, 115 {category: 'r', amount: 4.571}, 116 {category: 's', amount: 4.263}, 117 {category: 't', amount: 3.966}, 118 {category: 'u', amount: 2.347}, 119 {category: 'v', amount: 0.034}, 120 {category: 'w', amount: 4.549}, 121 {category: 'x', amount: 0.019}, 122 {category: 'y', amount: 3.857}, 123 {category: 'z', amount: 5.62}, 124 ], 125}; 126 127const DATA_EMPTY = {}; 128 129const SPEC_BAR_CHART = ` 130{ 131 "$schema": "https://vega.github.io/schema/vega/v5.json", 132 "description": "A basic bar chart example, with value labels shown upon mouse hover.", 133 "width": 400, 134 "height": 200, 135 "padding": 5, 136 137 "data": [ 138 { 139 "name": "table" 140 } 141 ], 142 143 "signals": [ 144 { 145 "name": "tooltip", 146 "value": {}, 147 "on": [ 148 {"events": "rect:mouseover", "update": "datum"}, 149 {"events": "rect:mouseout", "update": "{}"} 150 ] 151 } 152 ], 153 154 "scales": [ 155 { 156 "name": "xscale", 157 "type": "band", 158 "domain": {"data": "table", "field": "category"}, 159 "range": "width", 160 "padding": 0.05, 161 "round": true 162 }, 163 { 164 "name": "yscale", 165 "domain": {"data": "table", "field": "amount"}, 166 "nice": true, 167 "range": "height" 168 } 169 ], 170 171 "axes": [ 172 { "orient": "bottom", "scale": "xscale" }, 173 { "orient": "left", "scale": "yscale" } 174 ], 175 176 "marks": [ 177 { 178 "type": "rect", 179 "from": {"data":"table"}, 180 "encode": { 181 "enter": { 182 "x": {"scale": "xscale", "field": "category"}, 183 "width": {"scale": "xscale", "band": 1}, 184 "y": {"scale": "yscale", "field": "amount"}, 185 "y2": {"scale": "yscale", "value": 0} 186 }, 187 "update": { 188 "fill": {"value": "steelblue"} 189 }, 190 "hover": { 191 "fill": {"value": "red"} 192 } 193 } 194 }, 195 { 196 "type": "text", 197 "encode": { 198 "enter": { 199 "align": {"value": "center"}, 200 "baseline": {"value": "bottom"}, 201 "fill": {"value": "#333"} 202 }, 203 "update": { 204 "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5}, 205 "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2}, 206 "text": {"signal": "tooltip.amount"}, 207 "fillOpacity": [ 208 {"test": "datum === tooltip", "value": 0}, 209 {"value": 1} 210 ] 211 } 212 } 213 } 214 ] 215} 216`; 217 218const SPEC_BAR_CHART_LITE = ` 219{ 220 "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 221 "description": "A simple bar chart with embedded data.", 222 "data": { 223 "name": "table" 224 }, 225 "mark": "bar", 226 "encoding": { 227 "x": {"field": "category", "type": "nominal", "axis": {"labelAngle": 0}}, 228 "y": {"field": "amount", "type": "quantitative"} 229 } 230} 231`; 232 233const SPEC_BROKEN = `{ 234 "description": 123 235} 236`; 237 238enum SpecExample { 239 BarChart = 'Barchart', 240 BarChartLite = 'Barchart (Lite)', 241 Broken = 'Broken', 242} 243 244enum DataExample { 245 English = 'English', 246 Polish = 'Polish', 247 Empty = 'Empty', 248} 249 250function arg<T>( 251 anyArg: unknown, 252 valueIfTrue: T, 253 valueIfFalse: T | undefined = undefined, 254): T | undefined { 255 return Boolean(anyArg) ? valueIfTrue : valueIfFalse; 256} 257 258function getExampleSpec(example: SpecExample): string { 259 switch (example) { 260 case SpecExample.BarChart: 261 return SPEC_BAR_CHART; 262 case SpecExample.BarChartLite: 263 return SPEC_BAR_CHART_LITE; 264 case SpecExample.Broken: 265 return SPEC_BROKEN; 266 default: 267 const exhaustiveCheck: never = example; 268 throw new Error(`Unhandled case: ${exhaustiveCheck}`); 269 } 270} 271 272function getExampleData(example: DataExample) { 273 switch (example) { 274 case DataExample.English: 275 return DATA_ENGLISH_LETTER_FREQUENCY; 276 case DataExample.Polish: 277 return DATA_POLISH_LETTER_FREQUENCY; 278 case DataExample.Empty: 279 return DATA_EMPTY; 280 default: 281 const exhaustiveCheck: never = example; 282 throw new Error(`Unhandled case: ${exhaustiveCheck}`); 283 } 284} 285 286const options: {[key: string]: boolean} = { 287 foobar: false, 288 foo: false, 289 bar: false, 290 baz: false, 291 qux: false, 292 quux: false, 293 corge: false, 294 grault: false, 295 garply: false, 296 waldo: false, 297 fred: false, 298 plugh: false, 299 xyzzy: false, 300 thud: false, 301}; 302 303function PortalButton() { 304 let portalOpen = false; 305 306 return { 307 // eslint-disable-next-line @typescript-eslint/no-explicit-any 308 view: function ({attrs}: any) { 309 const {zIndex = true, absolute = true, top = true} = attrs; 310 return [ 311 m(Button, { 312 label: 'Toggle Portal', 313 intent: Intent.Primary, 314 onclick: () => { 315 portalOpen = !portalOpen; 316 }, 317 }), 318 portalOpen && 319 m( 320 Portal, 321 { 322 style: { 323 position: arg(absolute, 'absolute'), 324 top: arg(top, '0'), 325 zIndex: arg(zIndex, '10', '0'), 326 background: 'white', 327 }, 328 }, 329 m( 330 '', 331 `A very simple portal - a div rendered outside of the normal 332 flow of the page`, 333 ), 334 ), 335 ]; 336 }, 337 }; 338} 339 340function lorem() { 341 const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 342 tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 343 veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 344 commodo consequat.Duis aute irure dolor in reprehenderit in voluptate 345 velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 346 cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 347 est laborum.`; 348 return m('', {style: {width: '200px'}}, text); 349} 350 351function ControlledPopup() { 352 let popupOpen = false; 353 354 return { 355 view: function () { 356 return m( 357 Popup, 358 { 359 trigger: m(Button, {label: `${popupOpen ? 'Close' : 'Open'} Popup`}), 360 isOpen: popupOpen, 361 onChange: (shouldOpen: boolean) => (popupOpen = shouldOpen), 362 }, 363 m(Button, { 364 label: 'Close Popup', 365 onclick: () => { 366 popupOpen = !popupOpen; 367 }, 368 }), 369 ); 370 }, 371 }; 372} 373 374type Options = { 375 [key: string]: EnumOption | boolean | string | number; 376}; 377 378class EnumOption { 379 constructor( 380 public initial: string, 381 public options: string[], 382 ) {} 383} 384 385interface WidgetTitleAttrs { 386 label: string; 387} 388 389function recursiveTreeNode(): m.Children { 390 return m(LazyTreeNode, { 391 left: 'Recursive', 392 right: '...', 393 fetchData: async () => { 394 // await new Promise((r) => setTimeout(r, 1000)); 395 return () => recursiveTreeNode(); 396 }, 397 }); 398} 399 400class WidgetTitle implements m.ClassComponent<WidgetTitleAttrs> { 401 view({attrs}: m.CVnode<WidgetTitleAttrs>) { 402 const {label} = attrs; 403 const id = label.replaceAll(' ', '').toLowerCase(); 404 const href = `#!/widgets#${id}`; 405 return m(Anchor, {id, href}, m('h2', label)); 406 } 407} 408 409interface WidgetShowcaseAttrs { 410 label: string; 411 description?: string; 412 initialOpts?: Options; 413 // eslint-disable-next-line @typescript-eslint/no-explicit-any 414 renderWidget: (options: any) => any; 415 wide?: boolean; 416} 417 418// A little helper class to render any vnode with a dynamic set of options 419class WidgetShowcase implements m.ClassComponent<WidgetShowcaseAttrs> { 420 // eslint-disable-next-line @typescript-eslint/no-explicit-any 421 private optValues: any = {}; 422 private opts?: Options; 423 424 renderOptions(listItems: m.Child[]): m.Child { 425 if (listItems.length === 0) { 426 return null; 427 } 428 return m('.widget-controls', m('h3', 'Options'), m('ul', listItems)); 429 } 430 431 oninit({attrs: {initialOpts: opts}}: m.Vnode<WidgetShowcaseAttrs, this>) { 432 this.opts = opts; 433 if (opts) { 434 // Make the initial options values 435 for (const key in opts) { 436 if (Object.prototype.hasOwnProperty.call(opts, key)) { 437 const option = opts[key]; 438 if (option instanceof EnumOption) { 439 this.optValues[key] = option.initial; 440 } else { 441 this.optValues[key] = option; 442 } 443 } 444 } 445 } 446 } 447 448 view({attrs}: m.CVnode<WidgetShowcaseAttrs>) { 449 const {renderWidget, wide, label, description} = attrs; 450 const listItems = []; 451 452 if (this.opts) { 453 for (const key in this.opts) { 454 if (Object.prototype.hasOwnProperty.call(this.opts, key)) { 455 listItems.push(m('li', this.renderControlForOption(key))); 456 } 457 } 458 } 459 460 return [ 461 m(WidgetTitle, {label}), 462 description && m('p', description), 463 m( 464 '.widget-block', 465 m( 466 'div', 467 { 468 class: classNames( 469 'widget-container', 470 wide && 'widget-container-wide', 471 ), 472 }, 473 renderWidget(this.optValues), 474 ), 475 this.renderOptions(listItems), 476 ), 477 ]; 478 } 479 480 private renderControlForOption(key: string) { 481 if (!this.opts) return null; 482 const value = this.opts[key]; 483 if (value instanceof EnumOption) { 484 return this.renderEnumOption(key, value); 485 } else if (typeof value === 'boolean') { 486 return this.renderBooleanOption(key); 487 } else if (isString(value)) { 488 return this.renderStringOption(key); 489 } else if (typeof value === 'number') { 490 return this.renderNumberOption(key); 491 } else { 492 return null; 493 } 494 } 495 496 private renderBooleanOption(key: string) { 497 return m(Checkbox, { 498 checked: this.optValues[key], 499 label: key, 500 onchange: () => { 501 this.optValues[key] = !Boolean(this.optValues[key]); 502 }, 503 }); 504 } 505 506 private renderStringOption(key: string) { 507 return m( 508 'label', 509 `${key}:`, 510 m(TextInput, { 511 placeholder: key, 512 value: this.optValues[key], 513 oninput: (e: Event) => { 514 this.optValues[key] = (e.target as HTMLInputElement).value; 515 }, 516 }), 517 ); 518 } 519 520 private renderNumberOption(key: string) { 521 return m( 522 'label', 523 `${key}:`, 524 m(TextInput, { 525 type: 'number', 526 placeholder: key, 527 value: this.optValues[key], 528 oninput: (e: Event) => { 529 this.optValues[key] = Number.parseInt( 530 (e.target as HTMLInputElement).value, 531 ); 532 }, 533 }), 534 ); 535 } 536 537 private renderEnumOption(key: string, opt: EnumOption) { 538 const optionElements = opt.options.map((option: string) => { 539 return m('option', {value: option}, option); 540 }); 541 return m( 542 'label', 543 `${key}:`, 544 m( 545 Select, 546 { 547 value: this.optValues[key], 548 onchange: (e: Event) => { 549 const el = e.target as HTMLSelectElement; 550 this.optValues[key] = el.value; 551 }, 552 }, 553 optionElements, 554 ), 555 ); 556 } 557} 558 559interface File { 560 name: string; 561 size: string; 562 date: string; 563 children?: File[]; 564} 565 566const files: File[] = [ 567 { 568 name: 'foo', 569 size: '10MB', 570 date: '2023-04-02', 571 }, 572 { 573 name: 'bar', 574 size: '123KB', 575 date: '2023-04-08', 576 children: [ 577 { 578 name: 'baz', 579 size: '4KB', 580 date: '2023-05-07', 581 }, 582 { 583 name: 'qux', 584 size: '18KB', 585 date: '2023-05-28', 586 children: [ 587 { 588 name: 'quux', 589 size: '4KB', 590 date: '2023-05-07', 591 }, 592 { 593 name: 'corge', 594 size: '18KB', 595 date: '2023-05-28', 596 children: [ 597 { 598 name: 'grault', 599 size: '4KB', 600 date: '2023-05-07', 601 }, 602 { 603 name: 'garply', 604 size: '18KB', 605 date: '2023-05-28', 606 }, 607 { 608 name: 'waldo', 609 size: '87KB', 610 date: '2023-05-02', 611 }, 612 ], 613 }, 614 ], 615 }, 616 ], 617 }, 618 { 619 name: 'fred', 620 size: '8KB', 621 date: '2022-12-27', 622 }, 623]; 624 625let virtualTableData: {offset: number; rows: VirtualTableRow[]} = { 626 offset: 0, 627 rows: [], 628}; 629 630function TagInputDemo() { 631 const tags: string[] = ['foo', 'bar', 'baz']; 632 let tagInputValue: string = ''; 633 634 return { 635 view: () => { 636 return m(TagInput, { 637 tags, 638 value: tagInputValue, 639 onTagAdd: (tag) => { 640 tags.push(tag); 641 tagInputValue = ''; 642 }, 643 onChange: (value) => { 644 tagInputValue = value; 645 }, 646 onTagRemove: (index) => { 647 tags.splice(index, 1); 648 }, 649 }); 650 }, 651 }; 652} 653 654function SegmentedButtonsDemo({attrs}: {attrs: {}}) { 655 let selectedIdx = 0; 656 return { 657 view: () => { 658 return m(SegmentedButtons, { 659 ...attrs, 660 options: [{label: 'Yes'}, {label: 'Maybe'}, {label: 'No'}], 661 selectedOption: selectedIdx, 662 onOptionSelected: (num) => { 663 selectedIdx = num; 664 }, 665 }); 666 }, 667 }; 668} 669 670export class WidgetsPage implements m.ClassComponent<PageAttrs> { 671 view() { 672 return m( 673 '.widgets-page', 674 m('h1', 'Widgets'), 675 m(WidgetShowcase, { 676 label: 'Button', 677 renderWidget: ({label, icon, rightIcon, ...rest}) => 678 m(Button, { 679 icon: arg(icon, 'send'), 680 rightIcon: arg(rightIcon, 'arrow_forward'), 681 label: arg(label, 'Button', ''), 682 onclick: () => alert('button pressed'), 683 ...rest, 684 }), 685 initialOpts: { 686 label: true, 687 icon: true, 688 rightIcon: false, 689 disabled: false, 690 intent: new EnumOption(Intent.None, Object.values(Intent)), 691 active: false, 692 compact: false, 693 loading: false, 694 }, 695 }), 696 m(WidgetShowcase, { 697 label: 'Segmented Buttons', 698 description: ` 699 Segmented buttons are a group of buttons where one of them is 700 'selected'; they act similar to a set of radio buttons. 701 `, 702 renderWidget: (opts) => m(SegmentedButtonsDemo, opts), 703 initialOpts: { 704 disabled: false, 705 }, 706 }), 707 m(WidgetShowcase, { 708 label: 'Checkbox', 709 renderWidget: (opts) => m(Checkbox, {label: 'Checkbox', ...opts}), 710 initialOpts: { 711 disabled: false, 712 }, 713 }), 714 m(WidgetShowcase, { 715 label: 'Switch', 716 // eslint-disable-next-line @typescript-eslint/no-explicit-any 717 renderWidget: ({label, ...rest}: any) => 718 m(Switch, {label: arg(label, 'Switch'), ...rest}), 719 initialOpts: { 720 label: true, 721 disabled: false, 722 }, 723 }), 724 m(WidgetShowcase, { 725 label: 'Text Input', 726 renderWidget: ({placeholder, ...rest}) => 727 m(TextInput, { 728 placeholder: arg(placeholder, 'Placeholder...', ''), 729 ...rest, 730 }), 731 initialOpts: { 732 placeholder: true, 733 disabled: false, 734 }, 735 }), 736 m(WidgetShowcase, { 737 label: 'Select', 738 renderWidget: (opts) => 739 m(Select, opts, [ 740 m('option', {value: 'foo', label: 'Foo'}), 741 m('option', {value: 'bar', label: 'Bar'}), 742 m('option', {value: 'baz', label: 'Baz'}), 743 ]), 744 initialOpts: { 745 disabled: false, 746 }, 747 }), 748 m(WidgetShowcase, { 749 label: 'Empty State', 750 renderWidget: ({header, content}) => 751 m( 752 EmptyState, 753 { 754 title: arg(header, 'No search results found...'), 755 }, 756 arg(content, m(Button, {label: 'Try again'})), 757 ), 758 initialOpts: { 759 header: true, 760 content: true, 761 }, 762 }), 763 m(WidgetShowcase, { 764 label: 'Anchor', 765 renderWidget: ({icon}) => 766 m( 767 Anchor, 768 { 769 icon: arg(icon, 'open_in_new'), 770 href: 'https://perfetto.dev/docs/', 771 target: '_blank', 772 }, 773 'This is some really long text and it will probably overflow the container', 774 ), 775 initialOpts: { 776 icon: true, 777 }, 778 }), 779 m(WidgetShowcase, { 780 label: 'CopyableLink', 781 renderWidget: ({noicon}) => 782 m(CopyableLink, { 783 noicon: arg(noicon, true), 784 url: 'https://perfetto.dev/docs/', 785 }), 786 initialOpts: { 787 noicon: false, 788 }, 789 }), 790 m(WidgetShowcase, { 791 label: 'Table', 792 renderWidget: () => m(TableShowcase), 793 initialOpts: {}, 794 wide: true, 795 }), 796 m(WidgetShowcase, { 797 label: 'Portal', 798 description: `A portal is a div rendered out of normal flow 799 of the hierarchy.`, 800 renderWidget: (opts) => m(PortalButton, opts), 801 initialOpts: { 802 absolute: true, 803 zIndex: true, 804 top: true, 805 }, 806 }), 807 m(WidgetShowcase, { 808 label: 'Popup', 809 description: `A popup is a nicely styled portal element whose position is 810 dynamically updated to appear to float alongside a specific element on 811 the page, even as the element is moved and scrolled around.`, 812 renderWidget: (opts) => 813 m( 814 Popup, 815 { 816 trigger: m(Button, {label: 'Toggle Popup'}), 817 ...opts, 818 }, 819 lorem(), 820 ), 821 initialOpts: { 822 position: new EnumOption( 823 PopupPosition.Auto, 824 Object.values(PopupPosition), 825 ), 826 closeOnEscape: true, 827 closeOnOutsideClick: true, 828 }, 829 }), 830 m(WidgetShowcase, { 831 label: 'Controlled Popup', 832 description: `The open/close state of a controlled popup is passed in via 833 the 'isOpen' attribute. This means we can get open or close the popup 834 from wherever we like. E.g. from a button inside the popup. 835 Keeping this state external also means we can modify other parts of the 836 page depending on whether the popup is open or not, such as the text 837 on this button. 838 Note, this is the same component as the popup above, but used in 839 controlled mode.`, 840 renderWidget: (opts) => m(ControlledPopup, opts), 841 initialOpts: {}, 842 }), 843 m(WidgetShowcase, { 844 label: 'Icon', 845 renderWidget: (opts) => m(Icon, {icon: 'star', ...opts}), 846 initialOpts: {filled: false}, 847 }), 848 m(WidgetShowcase, { 849 label: 'MultiSelect panel', 850 renderWidget: ({...rest}) => 851 m(MultiSelect, { 852 options: Object.entries(options).map(([key, value]) => { 853 return { 854 id: key, 855 name: key, 856 checked: value, 857 }; 858 }), 859 onChange: (diffs: MultiSelectDiff[]) => { 860 diffs.forEach(({id, checked}) => { 861 options[id] = checked; 862 }); 863 }, 864 ...rest, 865 }), 866 initialOpts: { 867 repeatCheckedItemsAtTop: false, 868 fixedSize: false, 869 }, 870 }), 871 m(WidgetShowcase, { 872 label: 'Popup with MultiSelect', 873 renderWidget: ({icon, ...rest}) => 874 m(PopupMultiSelect, { 875 options: Object.entries(options).map(([key, value]) => { 876 return { 877 id: key, 878 name: key, 879 checked: value, 880 }; 881 }), 882 popupPosition: PopupPosition.Top, 883 label: 'Multi Select', 884 icon: arg(icon, Icons.LibraryAddCheck), 885 onChange: (diffs: MultiSelectDiff[]) => { 886 diffs.forEach(({id, checked}) => { 887 options[id] = checked; 888 }); 889 }, 890 ...rest, 891 }), 892 initialOpts: { 893 icon: true, 894 showNumSelected: true, 895 repeatCheckedItemsAtTop: false, 896 }, 897 }), 898 m(WidgetShowcase, { 899 label: 'Menu', 900 renderWidget: () => 901 m( 902 Menu, 903 m(MenuItem, {label: 'New', icon: 'add'}), 904 m(MenuItem, {label: 'Open', icon: 'folder_open'}), 905 m(MenuItem, {label: 'Save', icon: 'save', disabled: true}), 906 m(MenuDivider), 907 m(MenuItem, {label: 'Delete', icon: 'delete'}), 908 m(MenuDivider), 909 m( 910 MenuItem, 911 {label: 'Share', icon: 'share'}, 912 m(MenuItem, {label: 'Everyone', icon: 'public'}), 913 m(MenuItem, {label: 'Friends', icon: 'group'}), 914 m( 915 MenuItem, 916 {label: 'Specific people', icon: 'person_add'}, 917 m(MenuItem, {label: 'Alice', icon: 'person'}), 918 m(MenuItem, {label: 'Bob', icon: 'person'}), 919 ), 920 ), 921 m( 922 MenuItem, 923 {label: 'More', icon: 'more_horiz'}, 924 m(MenuItem, {label: 'Query', icon: 'database'}), 925 m(MenuItem, {label: 'Download', icon: 'download'}), 926 m(MenuItem, {label: 'Clone', icon: 'copy_all'}), 927 ), 928 ), 929 }), 930 m(WidgetShowcase, { 931 label: 'PopupMenu', 932 renderWidget: (opts) => 933 m( 934 PopupMenu, 935 { 936 trigger: m(Button, { 937 label: 'Menu', 938 rightIcon: Icons.ContextMenu, 939 }), 940 ...opts, 941 }, 942 m(MenuItem, {label: 'New', icon: 'add'}), 943 m(MenuItem, {label: 'Open', icon: 'folder_open'}), 944 m(MenuItem, {label: 'Save', icon: 'save', disabled: true}), 945 m(MenuDivider), 946 m(MenuItem, {label: 'Delete', icon: 'delete'}), 947 m(MenuDivider), 948 m( 949 MenuItem, 950 {label: 'Share', icon: 'share'}, 951 m(MenuItem, {label: 'Everyone', icon: 'public'}), 952 m(MenuItem, {label: 'Friends', icon: 'group'}), 953 m( 954 MenuItem, 955 {label: 'Specific people', icon: 'person_add'}, 956 m(MenuItem, {label: 'Alice', icon: 'person'}), 957 m(MenuItem, {label: 'Bob', icon: 'person'}), 958 ), 959 ), 960 m( 961 MenuItem, 962 {label: 'More', icon: 'more_horiz'}, 963 m(MenuItem, {label: 'Query', icon: 'database'}), 964 m(MenuItem, {label: 'Download', icon: 'download'}), 965 m(MenuItem, {label: 'Clone', icon: 'copy_all'}), 966 ), 967 ), 968 initialOpts: { 969 popupPosition: new EnumOption( 970 PopupPosition.Bottom, 971 Object.values(PopupPosition), 972 ), 973 }, 974 }), 975 m(WidgetShowcase, { 976 label: 'Spinner', 977 description: `Simple spinner, rotates forever. 978 Width and height match the font size.`, 979 renderWidget: ({fontSize, easing}) => 980 m('', {style: {fontSize}}, m(Spinner, {easing})), 981 initialOpts: { 982 fontSize: new EnumOption('16px', [ 983 '12px', 984 '16px', 985 '24px', 986 '32px', 987 '64px', 988 '128px', 989 ]), 990 easing: false, 991 }, 992 }), 993 m(WidgetShowcase, { 994 label: 'Tree', 995 description: `Hierarchical tree with left and right values aligned to 996 a grid.`, 997 renderWidget: (opts) => 998 m( 999 Tree, 1000 opts, 1001 m(TreeNode, {left: 'Name', right: 'my_event', icon: 'badge'}), 1002 m(TreeNode, {left: 'CPU', right: '2', icon: 'memory'}), 1003 m(TreeNode, { 1004 left: 'Start time', 1005 right: '1s 435ms', 1006 icon: 'schedule', 1007 }), 1008 m(TreeNode, {left: 'Duration', right: '86ms', icon: 'timer'}), 1009 m(TreeNode, { 1010 left: 'SQL', 1011 right: m( 1012 PopupMenu, 1013 { 1014 popupPosition: PopupPosition.RightStart, 1015 trigger: m( 1016 Anchor, 1017 { 1018 icon: Icons.ContextMenu, 1019 }, 1020 'SELECT * FROM ftrace_event WHERE id = 123', 1021 ), 1022 }, 1023 m(MenuItem, { 1024 label: 'Copy SQL Query', 1025 icon: 'content_copy', 1026 }), 1027 m(MenuItem, { 1028 label: 'Execute Query in new tab', 1029 icon: 'open_in_new', 1030 }), 1031 ), 1032 }), 1033 m(TreeNode, { 1034 icon: 'account_tree', 1035 left: 'Process', 1036 right: m(Anchor, {icon: 'open_in_new'}, '/bin/foo[789]'), 1037 }), 1038 m(TreeNode, { 1039 left: 'Thread', 1040 right: m(Anchor, {icon: 'open_in_new'}, 'my_thread[456]'), 1041 }), 1042 m( 1043 TreeNode, 1044 { 1045 left: 'Args', 1046 summary: 'foo: string, baz: string, quux: string[4]', 1047 }, 1048 m(TreeNode, {left: 'foo', right: 'bar'}), 1049 m(TreeNode, {left: 'baz', right: 'qux'}), 1050 m( 1051 TreeNode, 1052 {left: 'quux', summary: 'string[4]'}, 1053 m(TreeNode, {left: '[0]', right: 'corge'}), 1054 m(TreeNode, {left: '[1]', right: 'grault'}), 1055 m(TreeNode, {left: '[2]', right: 'garply'}), 1056 m(TreeNode, {left: '[3]', right: 'waldo'}), 1057 ), 1058 ), 1059 m(LazyTreeNode, { 1060 left: 'Lazy', 1061 icon: 'bedtime', 1062 fetchData: async () => { 1063 await new Promise((r) => setTimeout(r, 1000)); 1064 return () => m(TreeNode, {left: 'foo'}); 1065 }, 1066 }), 1067 m(LazyTreeNode, { 1068 left: 'Dynamic', 1069 unloadOnCollapse: true, 1070 icon: 'bedtime', 1071 fetchData: async () => { 1072 await new Promise((r) => setTimeout(r, 1000)); 1073 return () => m(TreeNode, {left: 'foo'}); 1074 }, 1075 }), 1076 recursiveTreeNode(), 1077 ), 1078 wide: true, 1079 }), 1080 m(WidgetShowcase, { 1081 label: 'Form', 1082 renderWidget: () => renderForm('form'), 1083 }), 1084 m(WidgetShowcase, { 1085 label: 'Nested Popups', 1086 renderWidget: () => 1087 m( 1088 Popup, 1089 { 1090 trigger: m(Button, {label: 'Open the popup'}), 1091 }, 1092 m( 1093 PopupMenu, 1094 { 1095 trigger: m(Button, {label: 'Select an option'}), 1096 }, 1097 m(MenuItem, {label: 'Option 1'}), 1098 m(MenuItem, {label: 'Option 2'}), 1099 ), 1100 m(Button, { 1101 label: 'Done', 1102 dismissPopup: true, 1103 }), 1104 ), 1105 }), 1106 m(WidgetShowcase, { 1107 label: 'Callout', 1108 renderWidget: () => 1109 m( 1110 Callout, 1111 { 1112 icon: 'info', 1113 }, 1114 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 1115 'Nulla rhoncus tempor neque, sed malesuada eros dapibus vel. ' + 1116 'Aliquam in ligula vitae tortor porttitor laoreet iaculis ' + 1117 'finibus est.', 1118 ), 1119 }), 1120 m(WidgetShowcase, { 1121 label: 'Editor', 1122 renderWidget: () => 1123 m(Editor, { 1124 language: 'perfetto-sql', 1125 onUpdate: (text) => { 1126 parseAndPrintTree(text); 1127 }, 1128 }), 1129 }), 1130 m(WidgetShowcase, { 1131 label: 'VegaView', 1132 renderWidget: (opt) => 1133 m(VegaView, { 1134 spec: getExampleSpec(opt.exampleSpec), 1135 data: getExampleData(opt.exampleData), 1136 }), 1137 initialOpts: { 1138 exampleSpec: new EnumOption( 1139 SpecExample.BarChart, 1140 Object.values(SpecExample), 1141 ), 1142 exampleData: new EnumOption( 1143 DataExample.English, 1144 Object.values(DataExample), 1145 ), 1146 }, 1147 }), 1148 m(WidgetShowcase, { 1149 label: 'Form within PopupMenu', 1150 description: `A form placed inside a popup menu works just fine, 1151 and the cancel/submit buttons also dismiss the popup. A bit more 1152 margin is added around it too, which improves the look and feel.`, 1153 renderWidget: () => 1154 m( 1155 PopupMenu, 1156 { 1157 trigger: m(Button, {label: 'Popup!'}), 1158 }, 1159 m( 1160 MenuItem, 1161 { 1162 label: 'Open form...', 1163 }, 1164 renderForm('popup-form'), 1165 ), 1166 ), 1167 }), 1168 m(WidgetShowcase, { 1169 label: 'Hotkey', 1170 renderWidget: (opts) => { 1171 if (opts.platform === 'auto') { 1172 return m(HotkeyGlyphs, {hotkey: opts.hotkey as Hotkey}); 1173 } else { 1174 const platform = opts.platform as Platform; 1175 return m(HotkeyGlyphs, { 1176 hotkey: opts.hotkey as Hotkey, 1177 spoof: platform, 1178 }); 1179 } 1180 }, 1181 initialOpts: { 1182 hotkey: 'Mod+Shift+P', 1183 platform: new EnumOption('auto', ['auto', 'Mac', 'PC']), 1184 }, 1185 }), 1186 m(WidgetShowcase, { 1187 label: 'Text Paragraph', 1188 description: `A basic formatted text paragraph with wrapping. If 1189 it is desirable to preserve the original text format/line breaks, 1190 set the compressSpace attribute to false.`, 1191 renderWidget: (opts) => { 1192 return m(TextParagraph, { 1193 text: `Lorem ipsum dolor sit amet, consectetur adipiscing 1194 elit. Nulla rhoncus tempor neque, sed malesuada eros 1195 dapibus vel. Aliquam in ligula vitae tortor porttitor 1196 laoreet iaculis finibus est.`, 1197 compressSpace: opts.compressSpace, 1198 }); 1199 }, 1200 initialOpts: { 1201 compressSpace: true, 1202 }, 1203 }), 1204 m(WidgetShowcase, { 1205 label: 'Multi Paragraph Text', 1206 description: `A wrapper for multiple paragraph widgets.`, 1207 renderWidget: () => { 1208 return m( 1209 MultiParagraphText, 1210 m(TextParagraph, { 1211 text: `Lorem ipsum dolor sit amet, consectetur adipiscing 1212 elit. Nulla rhoncus tempor neque, sed malesuada eros 1213 dapibus vel. Aliquam in ligula vitae tortor porttitor 1214 laoreet iaculis finibus est.`, 1215 compressSpace: true, 1216 }), 1217 m(TextParagraph, { 1218 text: `Sed ut perspiciatis unde omnis iste natus error sit 1219 voluptatem accusantium doloremque laudantium, totam rem 1220 aperiam, eaque ipsa quae ab illo inventore veritatis et 1221 quasi architecto beatae vitae dicta sunt explicabo. 1222 Nemo enim ipsam voluptatem quia voluptas sit aspernatur 1223 aut odit aut fugit, sed quia consequuntur magni dolores 1224 eos qui ratione voluptatem sequi nesciunt.`, 1225 compressSpace: true, 1226 }), 1227 ); 1228 }, 1229 }), 1230 m(WidgetShowcase, { 1231 label: 'Modal', 1232 description: `A helper for modal dialog.`, 1233 renderWidget: () => m(ModalShowcase), 1234 }), 1235 m(WidgetShowcase, { 1236 label: 'TreeTable', 1237 description: `Hierarchical tree with multiple columns`, 1238 renderWidget: () => { 1239 const attrs: TreeTableAttrs<File> = { 1240 rows: files, 1241 getChildren: (file) => file.children, 1242 columns: [ 1243 {name: 'Name', getData: (file) => file.name}, 1244 {name: 'Size', getData: (file) => file.size}, 1245 {name: 'Date', getData: (file) => file.date}, 1246 ], 1247 }; 1248 return m(TreeTable<File>, attrs); 1249 }, 1250 }), 1251 m(WidgetShowcase, { 1252 label: 'VirtualTable', 1253 description: `Virtualized table for efficient rendering of large datasets`, 1254 renderWidget: () => { 1255 const attrs: VirtualTableAttrs = { 1256 columns: [ 1257 {header: 'x', width: '4em'}, 1258 {header: 'x^2', width: '8em'}, 1259 ], 1260 rows: virtualTableData.rows, 1261 firstRowOffset: virtualTableData.offset, 1262 rowHeight: 20, 1263 numRows: 500_000, 1264 style: {height: '200px'}, 1265 onReload: (rowOffset, rowCount) => { 1266 const rows = []; 1267 for (let i = rowOffset; i < rowOffset + rowCount; i++) { 1268 rows.push({id: i, cells: [i, i ** 2]}); 1269 } 1270 virtualTableData = { 1271 offset: rowOffset, 1272 rows, 1273 }; 1274 }, 1275 }; 1276 return m(VirtualTable, attrs); 1277 }, 1278 }), 1279 m(WidgetShowcase, { 1280 label: 'Tag Input', 1281 description: ` 1282 TagInput displays Tag elements inside an input, followed by an 1283 interactive text input. The container is styled to look like a 1284 TextInput, but the actual editable element appears after the last tag. 1285 Clicking anywhere on the container will focus the text input.`, 1286 renderWidget: () => m(TagInputDemo), 1287 }), 1288 m(WidgetShowcase, { 1289 label: 'Middle Ellipsis', 1290 description: ` 1291 Sometimes the start and end of a bit of text are more important than 1292 the middle. This element puts the ellipsis in the midde if the content 1293 is too wide for its container.`, 1294 renderWidget: (opts) => 1295 m( 1296 'div', 1297 {style: {width: Boolean(opts.squeeze) ? '150px' : '450px'}}, 1298 m(MiddleEllipsis, { 1299 text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', 1300 }), 1301 ), 1302 initialOpts: { 1303 squeeze: false, 1304 }, 1305 }), 1306 m(WidgetShowcase, { 1307 label: 'Chip', 1308 description: `A little chip or tag`, 1309 renderWidget: (opts) => { 1310 const {icon, ...rest} = opts; 1311 return m( 1312 ChipBar, 1313 m(Chip, { 1314 label: 'Foo', 1315 icon: icon === true ? 'info' : undefined, 1316 ...rest, 1317 }), 1318 m(Chip, {label: 'Bar', ...rest}), 1319 m(Chip, {label: 'Baz', ...rest}), 1320 ); 1321 }, 1322 initialOpts: { 1323 intent: new EnumOption(Intent.None, Object.values(Intent)), 1324 icon: true, 1325 compact: false, 1326 rounded: false, 1327 }, 1328 }), 1329 m(WidgetShowcase, { 1330 label: 'TrackShell', 1331 description: `The Mithril parts of a track (the shell, mainly).`, 1332 renderWidget: (opts) => { 1333 const {buttons, chips, multipleTracks, error, ...rest} = opts; 1334 const dummyButtons = () => [ 1335 m(Button, {icon: 'info', compact: true}), 1336 m(Button, {icon: 'settings', compact: true}), 1337 ]; 1338 const dummyChips = () => ['foo', 'bar']; 1339 1340 const renderTrack = (children?: m.Children) => 1341 m( 1342 TrackShell, 1343 { 1344 buttons: Boolean(buttons) ? dummyButtons() : undefined, 1345 chips: Boolean(chips) ? dummyChips() : undefined, 1346 error: Boolean(error) 1347 ? new Error('An error has occurred') 1348 : undefined, 1349 ...rest, 1350 }, 1351 children, 1352 ); 1353 1354 return m( 1355 '', 1356 { 1357 style: {width: '500px', boxShadow: '0px 0px 1px 1px lightgray'}, 1358 }, 1359 Boolean(multipleTracks) 1360 ? [renderTrack(), renderTrack(), renderTrack()] 1361 : renderTrack(), 1362 ); 1363 }, 1364 initialOpts: { 1365 title: 'This is the title of the track', 1366 subtitle: 'This is the subtitle of the track', 1367 buttons: true, 1368 chips: true, 1369 heightPx: 32, 1370 collapsible: true, 1371 collapsed: true, 1372 summary: false, 1373 highlight: false, 1374 error: false, 1375 multipleTracks: false, 1376 reorderable: false, 1377 depth: 0, 1378 lite: false, 1379 }, 1380 }), 1381 m(WidgetShowcase, { 1382 label: 'Virtual Overlay Canvas', 1383 description: `A scrolling container that draws a virtual canvas over 1384 the top of it's content and keeps it in the viewport to make it appear 1385 like there is one big canvas over the top of the content.`, 1386 renderWidget: () => { 1387 const width = 200; 1388 const rowCount = 65536; 1389 const rowHeight = 20; 1390 return m( 1391 VirtualOverlayCanvas, 1392 { 1393 className: 'virtual-canvas', 1394 overflowY: 'auto', 1395 onCanvasRedraw({ctx, canvasRect}) { 1396 ctx.strokeStyle = 'red'; 1397 ctx.lineWidth = 1; 1398 1399 ctx.font = '20px Arial'; 1400 ctx.fillStyle = 'black'; 1401 ctx.textAlign = 'left'; 1402 ctx.textBaseline = 'top'; 1403 1404 for (let i = 0; i < rowCount; i++) { 1405 const rect = { 1406 left: 0, 1407 top: i * rowHeight, 1408 right: width, 1409 bottom: i * rowHeight + rowHeight, 1410 }; 1411 if (canvasRect.overlaps(rect)) { 1412 ctx.strokeRect(0, i * rowHeight, width, rowHeight); 1413 ctx.fillText(`Row: ${i}`, 0, i * rowHeight); 1414 } 1415 } 1416 }, 1417 }, 1418 m('', { 1419 style: {height: `${rowCount * rowHeight}px`, width: `${width}px`}, 1420 }), 1421 ); 1422 }, 1423 initialOpts: {}, 1424 }), 1425 1426 m(WidgetShowcase, { 1427 label: 'SplitPanel', 1428 description: `Horizontal split panel with draggable handle and controls.`, 1429 renderWidget: (opts) => { 1430 return m( 1431 '', 1432 {style: {height: '400px', width: '400px', border: 'solid 2px red'}}, 1433 m( 1434 SplitPanel, 1435 { 1436 drawerContent: 'Drawer Content', 1437 handleContent: Boolean(opts.handleContent) && 'Handle Content', 1438 }, 1439 'Main Content', 1440 ), 1441 ); 1442 }, 1443 initialOpts: { 1444 handleContent: false, 1445 }, 1446 }), 1447 1448 m(WidgetShowcase, { 1449 label: 'TabbedSplitPanel', 1450 description: `SplitPanel + tabs.`, 1451 renderWidget: (opts) => { 1452 return m( 1453 '', 1454 {style: {height: '400px', width: '400px', border: 'solid 2px red'}}, 1455 m( 1456 TabbedSplitPanel, 1457 { 1458 leftHandleContent: 1459 Boolean(opts.leftContent) && 1460 m(Button, {icon: 'Menu', compact: true}), 1461 tabs: [ 1462 { 1463 key: 'foo', 1464 title: 'Foo', 1465 content: 'Foo content', 1466 hasCloseButton: opts.showCloseButtons, 1467 }, 1468 { 1469 key: 'bar', 1470 title: 'Bar', 1471 content: 'Bar content', 1472 hasCloseButton: opts.showCloseButtons, 1473 }, 1474 ], 1475 }, 1476 'Main Content', 1477 ), 1478 ); 1479 }, 1480 initialOpts: { 1481 leftContent: true, 1482 showCloseButtons: true, 1483 }, 1484 }), 1485 ); 1486 } 1487} 1488 1489class ModalShowcase implements m.ClassComponent { 1490 private static counter = 0; 1491 1492 private static log(txt: string) { 1493 const mwlogs = document.getElementById('mwlogs'); 1494 if (!mwlogs || !(mwlogs instanceof HTMLTextAreaElement)) return; 1495 const time = new Date().toLocaleTimeString(); 1496 mwlogs.value += `[${time}] ${txt}\n`; 1497 mwlogs.scrollTop = mwlogs.scrollHeight; 1498 } 1499 1500 private static showModalDialog(staticContent = false) { 1501 const id = `N=${++ModalShowcase.counter}`; 1502 ModalShowcase.log(`Open ${id}`); 1503 const logOnClose = () => ModalShowcase.log(`Close ${id}`); 1504 1505 let content; 1506 if (staticContent) { 1507 content = m('.modal-pre', 'Content of the modal dialog.\nEnd of content'); 1508 } else { 1509 // The humble counter is basically the VDOM 'Hello world'! 1510 function CounterComponent() { 1511 let counter = 0; 1512 return { 1513 view: () => { 1514 return m( 1515 '', 1516 `Counter value: ${counter}`, 1517 m(Button, { 1518 intent: Intent.Primary, 1519 label: 'Increment Counter', 1520 onclick: () => ++counter, 1521 }), 1522 ); 1523 }, 1524 }; 1525 } 1526 content = () => m(CounterComponent); 1527 } 1528 const closePromise = showModal({ 1529 title: `Modal dialog ${id}`, 1530 buttons: [ 1531 {text: 'OK', action: () => ModalShowcase.log(`OK ${id}`)}, 1532 {text: 'Cancel', action: () => ModalShowcase.log(`Cancel ${id}`)}, 1533 { 1534 text: 'Show another now', 1535 action: () => ModalShowcase.showModalDialog(), 1536 }, 1537 { 1538 text: 'Show another in 2s', 1539 action: () => setTimeout(() => ModalShowcase.showModalDialog(), 2000), 1540 }, 1541 ], 1542 content, 1543 }); 1544 closePromise.then(logOnClose); 1545 } 1546 1547 view() { 1548 return m( 1549 'div', 1550 { 1551 style: { 1552 'display': 'flex', 1553 'flex-direction': 'column', 1554 'width': '100%', 1555 }, 1556 }, 1557 m('textarea', { 1558 id: 'mwlogs', 1559 readonly: 'readonly', 1560 rows: '8', 1561 placeholder: 'Logs will appear here', 1562 }), 1563 m('input[type=button]', { 1564 value: 'Show modal (static)', 1565 onclick: () => ModalShowcase.showModalDialog(true), 1566 }), 1567 m('input[type=button]', { 1568 value: 'Show modal (dynamic)', 1569 onclick: () => ModalShowcase.showModalDialog(false), 1570 }), 1571 ); 1572 } 1573} // class ModalShowcase 1574 1575function renderForm(id: string) { 1576 return m( 1577 Form, 1578 { 1579 submitLabel: 'Submit', 1580 submitIcon: 'send', 1581 cancelLabel: 'Cancel', 1582 resetLabel: 'Reset', 1583 onSubmit: () => window.alert('Form submitted!'), 1584 }, 1585 m(FormLabel, {for: `${id}-foo`}, 'Foo'), 1586 m(TextInput, {id: `${id}-foo`}), 1587 m(FormLabel, {for: `${id}-bar`}, 'Bar'), 1588 m(Select, {id: `${id}-bar`}, [ 1589 m('option', {value: 'foo', label: 'Foo'}), 1590 m('option', {value: 'bar', label: 'Bar'}), 1591 m('option', {value: 'baz', label: 'Baz'}), 1592 ]), 1593 ); 1594} 1595