├── .gitignore
├── .prettierrc
├── .vscode
├── settings.json
└── spellright.dict
├── LICENSE
├── README.md
├── docs
├── 0c35d18bf06992036b69.woff2
├── 0f170ea0ecc81d3b9ccc.jpg
├── 4d73cb90e394b34b7670.woff
├── bundle.js
├── bundle.js.map
├── favicon.ico
└── index.html
├── package.json
├── packages
├── example
│ ├── .gitignore
│ ├── package.json
│ ├── src
│ │ ├── app.ts
│ │ ├── assets
│ │ │ ├── Vincent_van_Gogh_-_Landscape_at_twilight_-_Google_Art_Project.jpg
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ └── favicon.ico
│ │ ├── components
│ │ │ ├── about
│ │ │ │ └── about-page.ts
│ │ │ ├── buttons
│ │ │ │ └── button-page.ts
│ │ │ ├── collections
│ │ │ │ └── collections-page.ts
│ │ │ ├── home
│ │ │ │ └── home-page.ts
│ │ │ ├── inputs
│ │ │ │ └── input-page.ts
│ │ │ ├── layout.ts
│ │ │ ├── map-editor
│ │ │ │ └── map-editor-page.ts
│ │ │ ├── misc
│ │ │ │ └── misc-page.ts
│ │ │ ├── modals
│ │ │ │ └── modal-page.ts
│ │ │ ├── pickers
│ │ │ │ └── picker-page.ts
│ │ │ ├── selections
│ │ │ │ └── selection-page.ts
│ │ │ └── timeline
│ │ │ │ └── timeline-page.ts
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── models
│ │ │ └── dashboard.ts
│ │ ├── services
│ │ │ └── dashboard-service.ts
│ │ └── utils
│ │ │ ├── jpg.d.ts
│ │ │ ├── json.d.ts
│ │ │ ├── png.d.ts
│ │ │ ├── svg.d.ts
│ │ │ └── utils.ts
│ ├── tsconfig.json
│ ├── tslint.json
│ └── webpack.config.js
└── lib
│ ├── .gitignore
│ ├── .npmignore
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── autocomplete.ts
│ ├── button.ts
│ ├── carousel.ts
│ ├── chip.ts
│ ├── code-block.ts
│ ├── collapsible.ts
│ ├── collection.ts
│ ├── dropdown.ts
│ ├── floating-action-button.ts
│ ├── icon.ts
│ ├── index.ts
│ ├── input-options.ts
│ ├── input.ts
│ ├── label.ts
│ ├── map-editor.ts
│ ├── material-box.ts
│ ├── modal.ts
│ ├── option.ts
│ ├── pagination.ts
│ ├── parallax.ts
│ ├── pickers.ts
│ ├── radio.ts
│ ├── search-select.ts
│ ├── select.ts
│ ├── styles
│ │ ├── codeblock.css
│ │ ├── input.css
│ │ ├── map-editor.css
│ │ ├── switch.css
│ │ └── timeline.css
│ ├── switch.ts
│ ├── tabs.ts
│ ├── timeline.ts
│ └── utils.ts
│ ├── tsconfig.json
│ └── tslint.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 | dist
63 | .rpt2_cache
64 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "singleQuote": true,
5 | "printWidth": 120,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "spellright.language": [
3 | "en"
4 | ],
5 | "spellright.documentTypes": [
6 | "markdown",
7 | "latex",
8 | "plaintext"
9 | ],
10 | "typescript.tsdk": "./packages/lib/node_modules/typescript/lib"
11 | }
--------------------------------------------------------------------------------
/.vscode/spellright.dict:
--------------------------------------------------------------------------------
1 | materialize-css
2 | Url
3 | Color
4 | materialize-css
5 | callback
6 | Vnode
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Erik Vullings
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 | # mithril-materialized
2 |
3 | A [materialize-css](https://materializecss.com) library, [available on npm](https://www.npmjs.com/package/mithril-materialized), for the Mithril framework (tested with v2.0.0-rc7 and higher, but presumably, it should work with v1.1.6 too), making it easier to use a Materialize theme in your application. This library provides you with Mithril components, wrapping around the materialize-css functionality.
4 |
5 | Supported components:
6 |
7 | - [Buttons](https://erikvullings.github.io/mithril-materialized/#!/buttons)
8 | - Button
9 | - FlatButton
10 | - RoundButton
11 | - SubmitButton
12 | - [Inputs](https://erikvullings.github.io/mithril-materialized/#!/inputs)
13 | - TextInput
14 | - TextArea
15 | - AutoComplete
16 | - UrlInput
17 | - EmailInput
18 | - NumberInput
19 | - ColorInput
20 | - RangeInput
21 | - Chips
22 | - [Pickers](https://erikvullings.github.io/mithril-materialized/#!/pickers)
23 | - DatePicker
24 | - TimePicker
25 | - [Selections](https://erikvullings.github.io/mithril-materialized/#!/selections)
26 | - Select
27 | - Options
28 | - RadioButtons
29 | - Switch
30 | - Dropdown
31 | - [Collections](https://erikvullings.github.io/mithril-materialized/#!/collections)
32 | - Basic, Link and Avatar Collections
33 | - Collapsible or accordion
34 | - [Others](https://erikvullings.github.io/mithril-materialized/#!/modals)
35 | - ModalPanel
36 | - MaterialBox
37 | - Carousel
38 | - Pagination
39 | - Parallax
40 | - Additional
41 | - Label
42 | - HelperText
43 | - Not from Materialize-CSS
44 | - CodeBlock
45 | - SearchSelect, a searchable select dropdown
46 | - [MapEditor](https://erikvullings.github.io/mithril-materialized/#!/map_editor)
47 | - [Timeline](https://erikvullings.github.io/mithril-materialized/#!/timeline)
48 |
49 | ## Usage instructions
50 |
51 | See the [documentation](https://erikvullings.github.io/mithril-materialized/index.html) for examples on how to use this library in your own application. Please note that the library does not include mithril, nor the materialize-css JavaScript or CSS, so you have to include them yourself, as documented.
52 |
53 | ## Build instructions
54 |
55 | This repository consists of two packages, combined using `lerna`: the `lib` package that is published to `npm`, as well as an `example` project which uses this library to display the Mithril components that it contains.
56 |
57 | To install the dependencies, you can use `npm i`, or, alternatively, use `pnpm m i` (assuming you have installed `pnpm` as alternative package manager using `npm i -g pnpm`) to perform a multi-repository install. Next, build everything using `npm start` and visit the documentation page on [http://localhost:1234](http://localhost:1234) in case port 1234 is not occupied already.
58 |
59 | ## CSS
60 |
61 | Although I've tried to limit the CSS adaptations to a minimum, I needed to tweak certain parts to make it look better. You can either copy them manually, or import them, e.g.
62 |
63 | ```ts
64 | import 'mithril-materialized/dist/index.css';
65 | ```
66 |
67 | Here are the styles I've added.
68 |
69 | ```css
70 | /* For the switch */
71 | .clear,
72 | .clear-10,
73 | .clear-15 {
74 | clear: both;
75 | /* overflow: hidden; Précaution pour IE 7 */
76 | }
77 | .clear-10 {
78 | margin-bottom: 10px;
79 | }
80 | .clear-15 {
81 | margin-bottom: 15px;
82 | }
83 |
84 | span.mandatory {
85 | margin-left: 5px;
86 | color: red;
87 | }
88 |
89 | label+.switch {
90 | margin-top: 1rem;
91 | }
92 |
93 | /* For the color input */
94 | input[type='color']:not(.browser-default) {
95 | margin: 0px 0 8px 0;
96 | /** Copied from input[type=number] */
97 | background-color: transparent;
98 | border: none;
99 | border-bottom: 1px solid #9e9e9e;
100 | border-radius: 0;
101 | outline: none;
102 | height: 3rem;
103 | width: 100%;
104 | font-size: 16px;
105 | padding: 0;
106 | -webkit-box-shadow: none;
107 | box-shadow: none;
108 | -webkit-box-sizing: content-box;
109 | box-sizing: content-box;
110 | -webkit-transition: border 0.3s, -webkit-box-shadow 0.3s;
111 | transition: border 0.3s, -webkit-box-shadow 0.3s;
112 | transition: box-shadow 0.3s, border 0.3s;
113 | transition: box-shadow 0.3s, border 0.3s, -webkit-box-shadow 0.3s;
114 | }
115 |
116 | /* For the options' label */
117 | .input-field.options > label {
118 | top: -2.5rem;
119 | }
120 |
121 | /* For the code block */
122 | .codeblock {
123 | margin: 1.5rem 0 2.5rem 0;
124 | }
125 | .codeblock > div {
126 | margin-bottom: 1rem;
127 | }
128 | .codeblock > label {
129 | display: inline-block;
130 | }
131 |
132 | ```
133 |
--------------------------------------------------------------------------------
/docs/0c35d18bf06992036b69.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/docs/0c35d18bf06992036b69.woff2
--------------------------------------------------------------------------------
/docs/0f170ea0ecc81d3b9ccc.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/docs/0f170ea0ecc81d3b9ccc.jpg
--------------------------------------------------------------------------------
/docs/4d73cb90e394b34b7670.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/docs/4d73cb90e394b34b7670.woff
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
Mithril Materialized
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-materialized",
3 | "version": "0.1.1",
4 | "private": true,
5 | "description": "A materialize library for mithril.",
6 | "scripts": {
7 | "clean:local": "rimraf -rf ./docs",
8 | "build": "cd packages/lib && pnpm run build",
9 | "build:example": "cd packages/example && pnpm run build",
10 | "build:domain": "pnpm run -r --parallel clean:docs && pnpm run -r --parallel build:domain",
11 | "start": "pnpm -r --parallel run dev",
12 | "clean": "npm run clean:local && pnpm clean",
13 | "dry-run": "pnpm dry-run",
14 | "patch-release": "npm run clean && npm run build && pnpm patch-release",
15 | "minor-release": "npm run clean && npm run build && pnpm minor-release"
16 | },
17 | "keywords": [
18 | "mithril",
19 | "materialize-css",
20 | "material design",
21 | "ui toolkit"
22 | ],
23 | "author": "Erik Vullings (http://www.tno.nl)",
24 | "license": "MIT",
25 | "devDependencies": {
26 | "pnpm": "10.11.0",
27 | "rimraf": "^6.0.1"
28 | }
29 | }
--------------------------------------------------------------------------------
/packages/example/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .cache
3 | .parcel-cache
4 |
--------------------------------------------------------------------------------
/packages/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "private": true,
4 | "version": "0.1.4",
5 | "description": "Example project voor the mithril-materialized library.",
6 | "scripts": {
7 | "clean": "rimraf ./.cache ../docs",
8 | "dev": "npm start",
9 | "start": "webpack serve --env development --open",
10 | "build": "webpack --env production",
11 | "build:domain": "npm run clean && pnpm run build",
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "keywords": [
15 | "example",
16 | "mithril",
17 | "typescript",
18 | "materialize-css"
19 | ],
20 | "author": "Erik Vullings ",
21 | "license": "MIT",
22 | "target": "web",
23 | "dependencies": {
24 | "material-icons": "^1.13.14",
25 | "materialize-css": "^1.0.0",
26 | "mithril": "^2.3.0",
27 | "mithril-materialized": "workspace:*",
28 | "tslib": "^2.8.1"
29 | },
30 | "devDependencies": {
31 | "@types/materialize-css": "^1.0.14",
32 | "@types/mithril": "^2.2.7",
33 | "css-loader": "^7.1.2",
34 | "css-minimizer-webpack-plugin": "^7.0.2",
35 | "dotenv-webpack": "^8.1.0",
36 | "html-webpack-plugin": "^5.6.3",
37 | "html-webpack-tags-plugin": "^3.0.2",
38 | "mini-css-extract-plugin": "^2.9.2",
39 | "rimraf": "^6.0.1",
40 | "style-loader": "^4.0.0",
41 | "ts-loader": "^9.5.2",
42 | "typescript": "^5.8.3",
43 | "webpack": "^5.99.8",
44 | "webpack-cli": "6.0.1",
45 | "webpack-dev-server": "^5.2.1"
46 | }
47 | }
--------------------------------------------------------------------------------
/packages/example/src/app.ts:
--------------------------------------------------------------------------------
1 | import 'materialize-css/dist/css/materialize.min.css';
2 | import 'materialize-css/dist/js/materialize.min.js';
3 | import 'material-icons/iconfont/filled.css';
4 | import 'mithril-materialized/index.css';
5 | import m from 'mithril';
6 | import { dashboardSvc } from './services/dashboard-service';
7 | // import '@materializecss/materialize/dist/css/materialize.min.css';
8 | // import '/home/erik/dev/mithril-materialized/node_modules/.pnpm/@materializecss+materialize@2.0.1-alpha/node_modules/@materializecss/materialize/dist/css/materialize.min.css';
9 |
10 | document.documentElement.setAttribute('lang', 'en');
11 |
12 | m.route(document.body, dashboardSvc.defaultRoute, dashboardSvc.routingTable);
13 |
--------------------------------------------------------------------------------
/packages/example/src/assets/Vincent_van_Gogh_-_Landscape_at_twilight_-_Google_Art_Project.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/packages/example/src/assets/Vincent_van_Gogh_-_Landscape_at_twilight_-_Google_Art_Project.jpg
--------------------------------------------------------------------------------
/packages/example/src/assets/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/packages/example/src/assets/favicon-16x16.png
--------------------------------------------------------------------------------
/packages/example/src/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/packages/example/src/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/packages/example/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/packages/example/src/assets/favicon.ico
--------------------------------------------------------------------------------
/packages/example/src/components/about/about-page.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 |
3 | export const AboutPage = () => ({
4 | view: () =>
5 | m('.row', [
6 | m('h1', 'About'),
7 | m('h1', 'Attribution'),
8 | m('ul.collection', [m('li.collection-item', 'Logo: ideation by Vytautas Alech from the Noun Project.')]),
9 | ]),
10 | });
11 |
--------------------------------------------------------------------------------
/packages/example/src/components/buttons/button-page.ts:
--------------------------------------------------------------------------------
1 | import {
2 | RoundIconButton,
3 | SubmitButton,
4 | Button,
5 | FlatButton,
6 | FloatingActionButton,
7 | CodeBlock,
8 | } from 'mithril-materialized';
9 | import m, { Component } from 'mithril';
10 |
11 | export const ButtonPage = () => {
12 | const onclick = () => alert('Button clicked');
13 | return {
14 | view: () =>
15 | m('.col.s12', [
16 | m('h2.header', 'Buttons'),
17 |
18 | m('h3.header[id=fab]', 'Floating Action Button (FAB)'),
19 | m(FloatingActionButton, {
20 | className: 'red',
21 | iconName: 'mode_edit',
22 | direction: 'left',
23 | position: 'inline-right',
24 | buttons: [
25 | { iconName: 'insert_chart', className: 'red', onClick: () => console.log('Insert chart') },
26 | { iconName: 'format_quote', className: 'yellow darken-1', onClick: () => console.log('Format quote') },
27 | { iconName: 'publish', className: 'green', onClick: () => console.log('Publish') },
28 | { iconName: 'attach_file', className: 'blue', onClick: () => console.log('Attach file') },
29 | ],
30 | }),
31 | m(FloatingActionButton, {
32 | className: 'red',
33 | iconName: 'mode_edit',
34 | direction: 'left',
35 | buttons: [
36 | { iconName: 'insert_chart', className: 'red', onClick: () => console.log('Insert chart') },
37 | { iconName: 'format_quote', className: 'yellow darken-1', onClick: () => console.log('Format quote') },
38 | { iconName: 'publish', className: 'green', onClick: () => console.log('Publish') },
39 | { iconName: 'attach_file', className: 'blue', onClick: () => console.log('Attach file') },
40 | ],
41 | }),
42 | m(CodeBlock, {
43 | code: [
44 | `m(FloatingActionButton, {
45 | className: 'red',
46 | iconName: 'mode_edit',
47 | direction: 'left',
48 | position: 'inline-right', // Comment this out to get a FAB in the bottom-left of the page.
49 | buttons: [
50 | { iconName: 'insert_chart', className: 'red', onClick: () => console.log('Insert chart') },
51 | { iconName: 'format_quote', className: 'yellow darken-1', onClick: () => console.log('Format quote') },
52 | { iconName: 'publish', className: 'green', onClick: () => console.log('Publish') },
53 | { iconName: 'attach_file', className: 'blue', onClick: () => console.log('Attach file') },
54 | ],
55 | }),`,
56 | ],
57 | }),
58 |
59 | m('h3.header[id=raised]', 'Raised'),
60 | m('div', [
61 | m(Button, { label: 'First Button', onclick }),
62 | m(Button, { label: 'Second Button', iconName: 'cloud', onclick }),
63 | m(Button, { label: 'Third Button', iconName: 'cloud', iconClass: 'right', onclick }),
64 | m(Button, {
65 | label: 'Fourth Button',
66 | iconName: 'cloud',
67 | attr: { disabled: true },
68 | onclick,
69 | }),
70 | ]),
71 | m(CodeBlock, {
72 | code: [
73 | `const onclick = () => alert('Button clicked');
74 | m('div', [
75 | m(Button, { label: 'First Button', onclick }),
76 | m(Button, { label: 'Second Button', iconName: 'cloud', onclick }),
77 | m(Button, { label: 'Third Button', iconName: 'cloud', iconClass: 'right', onclick }),
78 | m(Button, {
79 | label: 'Fourth Button',
80 | iconName: 'cloud',
81 | attr: { disabled: true },
82 | onclick,
83 | }),
84 | ])`,
85 | ],
86 | }),
87 | m('h3.header[id=flatbutton]', 'FlatButton'),
88 | m('div', m(FlatButton, { label: 'My Flat button', onclick })),
89 | m(CodeBlock, { code: 'm(FlatButton, { label: "My Flat button", onclick })' }),
90 | m('h3.header[id=roundiconbutton]', 'RoundIconButton'),
91 | m('div', m(RoundIconButton, { iconName: 'create', onclick })),
92 | m(CodeBlock, { code: 'm(RoundIconButton, { iconName: "create", onclick })' }),
93 | m('h3.header[id=submitbutton]', 'SubmitButton'),
94 | m(
95 | 'div',
96 | m(SubmitButton, {
97 | label: 'Submit',
98 | iconName: 'send',
99 | iconClass: 'right',
100 | onclick,
101 | })
102 | ),
103 | m(CodeBlock, {
104 | code: `m(SubmitButton, {
105 | label: 'Submit',
106 | iconName: 'send',
107 | iconClass: 'right',
108 | onclick,
109 | })`,
110 | }),
111 | ]),
112 | } as Component;
113 | };
114 |
--------------------------------------------------------------------------------
/packages/example/src/components/collections/collections-page.ts:
--------------------------------------------------------------------------------
1 | import { CodeBlock, Collapsible, Collection, ICollectionItem, CollectionMode } from 'mithril-materialized';
2 | import m from 'mithril';
3 |
4 | const onclick = (item: ICollectionItem) => alert(`You clicked ${item.title}.`);
5 |
6 | export const CollectionsPage = () => {
7 | return {
8 | view: () =>
9 | m('.col.s12.m8.xl7', [
10 | m('h2.header', 'Collections and collapsible'),
11 | m('p', [
12 | 'For more information, see ',
13 | m('a[href=https://materializecss.com/collections.html][target=_blank]', 'Collections'),
14 | ' and ',
15 | m('a[href=https://materializecss.com/collapsible.html][target=_blank]', 'Collapsible'),
16 | '.',
17 | ]),
18 |
19 | m('h3.header', 'Secondary Content Collection'),
20 | m(Collection, {
21 | items: [
22 | { id: '1', title: 'John', iconName: 'send', onclick },
23 | { id: '2', title: 'Mary', iconName: 'send', onclick },
24 | { id: '3', title: 'Pete', iconName: 'send', onclick },
25 | ],
26 | }),
27 | m(CodeBlock, {
28 | code: ` m(Collection, {
29 | items: [
30 | // id is used as key, e.g. when sorting or editing the collection.
31 | { id: '1', title: 'John', iconName: 'send', onclick },
32 | { id: '2', title: 'Mary', iconName: 'send', onclick },
33 | { id: '3', title: 'Pete', iconName: 'send', onclick },
34 | ],
35 | })`,
36 | }),
37 |
38 | m('h3.header', 'Links collection'),
39 | m(Collection, {
40 | header: 'First names',
41 | mode: CollectionMode.LINKS,
42 | items: [
43 | { title: 'John', onclick: console.log },
44 | { title: 'Mary', onclick: console.log, href: '/timeline' },
45 | { title: 'Pete', onclick: console.log, href: 'https://www.google.com' },
46 | ],
47 | }),
48 | m(CodeBlock, {
49 | code: ` m(Collection, {
50 | header: 'First names',
51 | mode: CollectionMode.LINKS,
52 | items: [
53 | { title: 'John', onclick: console.log },
54 | { title: 'Mary', onclick: console.log, href: '/timeline' },
55 | { title: 'Pete', onclick: console.log, href: 'https://www.google.com' },
56 | ],
57 | })`,
58 | }),
59 |
60 | m('h3.header', 'Avatar collection'),
61 | m(Collection, {
62 | header: 'First names',
63 | mode: CollectionMode.AVATAR,
64 | items: [
65 | {
66 | title: 'John',
67 | content: 'First line
Second line',
68 | avatar: 'folder',
69 | className: 'green',
70 | iconName: 'grade',
71 | onclick,
72 | },
73 | {
74 | title: 'Mary',
75 | content: 'First line
Second line',
76 | avatar: 'https://pbs.twimg.com/profile_images/665673789112516608/v9itf6uk_400x400.jpg',
77 | iconName: 'grade',
78 | onclick,
79 | },
80 | {
81 | title: 'Pete',
82 | content: 'First line
Second line',
83 | avatar: 'play_arrow',
84 | className: 'red',
85 | iconName: 'file_download',
86 | href: 'http://www.google.com',
87 | },
88 | ],
89 | }),
90 | m(CodeBlock, {
91 | code: ` m(Collection, {
92 | header: 'First names',
93 | mode: CollectionMode.AVATAR,
94 | items: [
95 | {
96 | title: 'John',
97 | content: 'First line
Second line',
98 | avatar: 'folder',
99 | className: 'green',
100 | iconName: 'grade',
101 | onclick,
102 | },
103 | {
104 | title: 'Mary',
105 | content: 'First line
Second line',
106 | avatar: 'https://pbs.twimg.com/profile_images/665673789112516608/v9itf6uk_400x400.jpg',
107 | iconName: 'grade',
108 | onclick,
109 | },
110 | {
111 | title: 'Pete',
112 | content: 'First line
Second line',
113 | avatar: 'play_arrow',
114 | className: 'red',
115 | iconName: 'file_download',
116 | href: 'http://www.google.com',
117 | },
118 | ],
119 | })`,
120 | }),
121 |
122 | m('h3.header', 'Collapsible (accordion)'),
123 | m(
124 | '.row',
125 | m(Collapsible, {
126 | id: 'testme',
127 | className: 'first-second-third',
128 | items: [
129 | { id: 1, header: 'First', body: 'Lorem ipsum dolor sit amet.', iconName: 'filter_drama' },
130 | { id: 2, header: 'Second', body: 'Lorem ipsum dolor sit amet.', iconName: 'place', active: true },
131 | { id: 3, header: 'Third', body: m('span', 'Third in a span.'), iconName: 'whatshot' },
132 | ],
133 | })
134 | ),
135 | m(CodeBlock, {
136 | code: ` m(Collapsible, { items: [
137 | { header: 'First', body: 'Lorem ipsum dolor sit amet.', iconName: 'filter_drama' },
138 | { header: 'Second', body: 'Lorem ipsum dolor sit amet.', iconName: 'place', active: true },
139 | { header: 'Third', body: m('span', 'Third in a span.'), iconName: 'whatshot' },
140 | ] })`,
141 | }),
142 |
143 | m('h3.header', 'Collapsible (no accordion)'),
144 | m(
145 | '.row',
146 | m(Collapsible, {
147 | accordion: false,
148 | items: [
149 | { id: 1, header: 'First', body: 'Lorem ipsum dolor sit amet.', iconName: 'filter_drama', active: true },
150 | { id: 2, header: 'Second', body: 'Lorem ipsum dolor sit amet.', iconName: 'place', active: true },
151 | { id: 3, header: 'Third', body: m('span', 'Third in a span.'), iconName: 'whatshot' },
152 | ],
153 | })
154 | ),
155 | m(CodeBlock, {
156 | code: ` m(Collapsible, {
157 | accordion: false,
158 | items: [
159 | { header: 'First', body: 'Lorem ipsum dolor sit amet.', iconName: 'filter_drama', active: true },
160 | { header: 'Second', body: 'Lorem ipsum dolor sit amet.', iconName: 'place', active: true },
161 | { header: 'Third', body: m('span', 'Third in a span.'), iconName: 'whatshot' },
162 | ] })`,
163 | }),
164 | ]),
165 | };
166 | };
167 |
--------------------------------------------------------------------------------
/packages/example/src/components/home/home-page.ts:
--------------------------------------------------------------------------------
1 | import { dashboardSvc } from '../../services/dashboard-service';
2 | import m from 'mithril';
3 | import { CodeBlock } from 'mithril-materialized';
4 |
5 | export const HomePage = () => ({
6 | view: () =>
7 | m('.home-page', [
8 | m(
9 | '.col.s12.m7.l8',
10 | m('.introduction', [
11 | m('h2', 'About Mithril-Materialized'),
12 | m(
13 | 'p',
14 | `I like Mithril, and I also like materialize-css. However, to create some materialized components
15 | is a bit cumbersome as it requires a lot of HTML elements and a specific nesting which can easily go
16 | wrong. For that reason, the mithril-materialized library provides you with several ready-made
17 | Mithril components, so you can easily use them in your own application.`
18 | ),
19 | m('p', [
20 | 'You can check out the API documentation ',
21 | m('a[href="https://erikvullings.github.io/mithril-materialized/typedoc/index.html"]', 'here'),
22 | '.',
23 | ]),
24 | m('h3', 'Installation'),
25 | m('p', 'First, you need to install the required packages:'),
26 | m(CodeBlock, {
27 | language: 'console',
28 | code: `npm i materialize-css material-icons mithril mithril-materialized
29 | # Also install the typings if you use TypeScript
30 | npm i --save-dev @types/materialize-css @types/mithril`,
31 | }),
32 | m('p', 'Next, you can use them inside your application:'),
33 | m(CodeBlock, {
34 | code: `import 'materialize-css/dist/css/materialize.min.css';
35 | import 'material-icons/iconfont/material-icons.css';
36 | import { TextArea } from 'mithril-materialized';
37 | `,
38 | }),
39 | ])
40 | ),
41 | m('.col.s12.m5.l4', [
42 | m('h1', 'Contents'),
43 | m('ul.collection', [
44 | dashboardSvc
45 | .getList()
46 | .filter((d) => d.visible && !d.default)
47 | .map((d) => m('li.collection-item', m('a', { href: `#!${d.route}` }, d.title))),
48 | ]),
49 | ]),
50 | ]),
51 | });
52 |
--------------------------------------------------------------------------------
/packages/example/src/components/layout.ts:
--------------------------------------------------------------------------------
1 | import m, { Vnode } from 'mithril';
2 | import { dashboardSvc } from '../services/dashboard-service';
3 |
4 | const isActive = (path: string) => (m.route.get().indexOf(path) >= 0 ? '.active' : '');
5 |
6 | export const Layout = () => ({
7 | view: (vnode: Vnode) =>
8 | m('.main', [
9 | m(
10 | 'nav',
11 | m('.nav-wrapper', [
12 | m(
13 | // tslint:disable-next-line:max-line-length
14 | 'a.github-corner[aria-label=View source on GitHub][href=https://github.com/erikvullings/mithril-materialized]',
15 | m(
16 | 'svg[aria-hidden=true][height=80][viewBox=0 0 250 250][width=80]',
17 | {
18 | style: {
19 | fill: 'black',
20 | color: '#fff',
21 | position: 'absolute',
22 | top: '0',
23 | border: '0',
24 | left: '0',
25 | transform: 'scale(-1, 1)',
26 | },
27 | },
28 | [
29 | m('path[d=M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z]'),
30 | m(
31 | // tslint:disable-next-line:max-line-length
32 | 'path.octo-arm[d=M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2][fill=currentColor]',
33 | { style: { 'transform-origin': '130px 106px' } }
34 | ),
35 | m(
36 | // tslint:disable-next-line:max-line-length
37 | 'path.octo-body[d=M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z][fill=currentColor]'
38 | ),
39 | ]
40 | )
41 | ),
42 | m(
43 | 'style',
44 | // tslint:disable-next-line:max-line-length
45 | '.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}'
46 | ),
47 | m(
48 | 'ul.right',
49 | dashboardSvc
50 | .getList()
51 | .filter(d => d.visible)
52 | .map(d =>
53 | m(
54 | `li${isActive(d.route)}`,
55 | m(
56 | m.route.Link,
57 | { href: d.route },
58 | m('i.material-icons.right', d.icon ? m('i.material-icons', d.icon) : d.title)
59 | )
60 | )
61 | )
62 | ),
63 | ])
64 | ),
65 | m('.container', m('.row', vnode.children)),
66 | ]),
67 | });
68 |
--------------------------------------------------------------------------------
/packages/example/src/components/map-editor/map-editor-page.ts:
--------------------------------------------------------------------------------
1 | import { CodeBlock, MapEditor } from 'mithril-materialized';
2 | import m from 'mithril';
3 |
4 | export const MapEditorPage = () => {
5 | const state = {
6 | properties: {
7 | stringArray: ['a', 'b', 'c'],
8 | numberArray: [1, 2, 3],
9 | aNumber: 42,
10 | aString: 'Hello world',
11 | truthy: true,
12 | falsy: false,
13 | },
14 | };
15 |
16 | return {
17 | view: () =>
18 | m('.col.s12', [
19 | m('h2.header', 'Key-value pairs editor'),
20 | m('p', [
21 | 'As materializecss.com did not offer a useful editor for a map of key-value pairs, ',
22 | 'I have created one myself. It allows you to edit (or just view, when it is disabled), ',
23 | 'booleans, numbers, strings and arrays of numbers and strings.',
24 | ]),
25 |
26 | // m(EditableTable, {
27 | // headers: ['title', 'description', 'priority'],
28 | // cells,
29 | // addRows: true,
30 | // }),
31 |
32 | m('h3.header', 'MapEditor'),
33 | m(
34 | '.row',
35 | m(MapEditor, {
36 | label: 'Properties',
37 | isMandatory: true,
38 | properties: state.properties,
39 | onchange: (props: { [key: string]: number | string | boolean | Array }) =>
40 | console.table(props),
41 | labelKey: 'Unique key', // Override the default label for keys i.e. 'key'
42 | labelValue: 'My value', // Overrule the default label for values i.e. 'key'
43 | disable: false, // If true, the map cannot be edited
44 | disallowArrays: false, // If true, do not convert [1, 2, 3] to a number[]
45 | keyValueConverter: undefined, // Allows you to overrule the view of each key-value pair
46 | iconName: 'dns',
47 | keyClass: '.col.s4', // Optionally override the default key element
48 | valueClass: '.col.s8', // Optionally override the default value element
49 | truthy: ['true', 'yes', 'ja', 'oui', 'si', 'da'],
50 | falsy: ['false', 'no', 'nee', 'nein', 'non', 'nu', 'njet'],
51 | })
52 | ),
53 | m(CodeBlock, {
54 | code: ` m(MapEditor, {
55 | label: 'Properties',
56 | isMandatory: true,
57 | properties: state.properties,
58 | onchange: (props: any) => console.table(props),
59 | labelKey: 'Unique key', // Override the default label for keys i.e. 'key'
60 | labelValue: 'My value', // Overrule the default label for values i.e. 'key'
61 | disable: false, // If true, the map cannot be edited
62 | disallowArrays: false, // If true, do not convert [1, 2, 3] to a number[]
63 | keyValueConverter: undefined, // Allows you to overrule the view of each key-value pair
64 | iconName: 'dns',
65 | keyClass: '.col.s4', // Optionally override the default key element
66 | valueClass: '.col.s8', // Optionally override the default value element
67 | truthy: ['true', 'yes', 'ja', 'oui', 'si', 'da'], // Any truthy value generates a boolean
68 | falsy: ['false', 'no', 'nee', 'nein', 'non', 'nu', 'njet'],
69 | })`,
70 | }),
71 | ]),
72 | };
73 | };
74 |
--------------------------------------------------------------------------------
/packages/example/src/components/misc/misc-page.ts:
--------------------------------------------------------------------------------
1 | import { MaterialBox, CodeBlock, Carousel, Parallax, Pagination, Tabs, Button } from 'mithril-materialized';
2 | import m from 'mithril';
3 | import gogh from '../../assets/Vincent_van_Gogh_-_Landscape_at_twilight_-_Google_Art_Project.jpg';
4 |
5 | export const MiscPage = () => {
6 | const state = {
7 | activeTabId: '',
8 | disabled: true,
9 | activeTab: 3,
10 | tabWidthId: 2,
11 | tabWidths: ['auto', 'fixed', 'fill'] as Array<'auto' | 'fixed' | 'fill'>,
12 | };
13 | const curPage = () => (m.route.param('page') ? +m.route.param('page') : 1);
14 |
15 | return {
16 | view: () =>
17 | m('.col.s12', [
18 | m('h2.header', 'Miscellaneous'),
19 | m('p', [
20 | 'Some miscellaneous components, like the ',
21 | m('a[href=https://materializecss.com/tabs.html][target=_blank]', 'Tabs'),
22 | ', ',
23 | m('a[href=https://materializecss.com/media.html][target=_blank]', 'Material box'),
24 | ', ',
25 | m('a[href=https://materializecss.com/collection.html][target=_blank]', 'Collection'),
26 | ', ',
27 | m('a[href=https://materializecss.com/collapsible.html][target=_blank]', 'Collapsible'),
28 | ', ',
29 | m('a[href=https://materializecss.com/carousel.html][target=_blank]', 'Carousel'),
30 | ', ',
31 | m('a[href=https://materializecss.com/parallax.html][target=_blank]', 'Pagination'),
32 | ' and the ',
33 | m('a[href=https://materializecss.com/pagination.html][target=_blank]', 'Parallax'),
34 | '.',
35 | ]),
36 |
37 | m('h3.header', 'Tabs'),
38 | m(Tabs, {
39 | selectedTabId: state.activeTabId,
40 | tabWidth: state.tabWidths[state.tabWidthId % 3],
41 | onShow: console.log,
42 | tabs: [
43 | {
44 | title: 'Test 1',
45 | active: state.activeTab === 1,
46 | vnode: m('', 'Show content of tab 1'),
47 | },
48 | {
49 | title: 'Test 2',
50 | disabled: state.disabled,
51 | active: state.activeTab === 2,
52 | vnode: m('', 'Show content of tab 2'),
53 | },
54 | {
55 | title: 'Test 3',
56 | active: state.activeTab === 3,
57 | vnode: m('', 'Show content of tab 3'),
58 | },
59 | {
60 | title: 'Test 4',
61 | active: state.activeTab === 4,
62 | vnode: m('', 'Show content of tab 4'),
63 | },
64 | {
65 | title: 'Visit Google',
66 | target: '_blank',
67 | href: 'http://www.google.com',
68 | // vnode: m('', 'Nothing to show'),
69 | },
70 | ],
71 | }),
72 | m(Button, {
73 | label: 'Switch to tab 1',
74 | onclick: () => {
75 | state.activeTab = 1;
76 | state.activeTabId = '';
77 | },
78 | }),
79 | m(Button, {
80 | label: 'Switch to tab 4',
81 | onclick: () => {
82 | state.activeTab = 0;
83 | state.activeTabId = 'test4';
84 | },
85 | }),
86 | m(Button, {
87 | label: `${state.disabled ? 'Enable' : 'Disable'} tab 2`,
88 | onclick: () => {
89 | state.disabled = !state.disabled;
90 | },
91 | }),
92 | m(Button, {
93 | label: `Switch tab width from ${state.tabWidths[state.tabWidthId % 3]} to ${
94 | state.tabWidths[(state.tabWidthId + 1) % 3]
95 | }`,
96 | onclick: () => state.tabWidthId++,
97 | }),
98 | m(CodeBlock, {
99 | code: ` m(Tabs, {
100 | onShow: console.log,
101 | tabs: [
102 | {
103 | title: 'Test 1',
104 | vnode: m('', 'Show content of tab 1'),
105 | },
106 | {
107 | title: 'Test 2',
108 | disabled: true,
109 | vnode: m('', 'Show content of tab 2'),
110 | },
111 | {
112 | title: 'Test 3',
113 | active: true,
114 | vnode: m('', 'Show content of tab 3'),
115 | },
116 | {
117 | title: 'Test 4',
118 | vnode: m('', 'Show content of tab 4'),
119 | },
120 | {
121 | title: 'Visit Google',
122 | target: '_blank',
123 | href: 'http://www.google.com',
124 | },
125 | ],
126 | })`,
127 | }),
128 |
129 | m('h3.header', 'Parallax'),
130 | m(Parallax, { src: gogh }),
131 | m(CodeBlock, {
132 | code: ` m(Parallax, { src: gogh }) // should be embedded in layout so the width is not limited`,
133 | }),
134 |
135 | m('h3.header', 'Material box (click on image)'),
136 | m('.row', m(MaterialBox, { src: gogh, width: 600 })),
137 | m(CodeBlock, {
138 | code: ` m(MaterialBox, { src: gogh, width: 600 })`,
139 | }),
140 |
141 | m('h3.header', 'Carousel'),
142 | m(
143 | '.row',
144 | m(Carousel, {
145 | items: [
146 | { href: '#!/one!', src: 'https://picsum.photos/id/301/200/300' },
147 | { href: '#!/two!', src: 'https://picsum.photos/id/302/200/300' },
148 | { href: '#!/three!', src: 'https://picsum.photos/id/306/200/300' },
149 | { href: '#!/four!', src: 'https://picsum.photos/id/304/200/300' },
150 | { href: '#!/five!', src: 'https://picsum.photos/id/305/200/300' },
151 | ],
152 | })
153 | ),
154 | m(CodeBlock, {
155 | code: ` m(Carousel, { items: [
156 | { href: '#!/one!', src: 'https://picsum.photos/id/301/200/300' },
157 | { href: '#!/two!', src: 'https://picsum.photos/id/302/200/300' },
158 | { href: '#!/three!', src: 'https://picsum.photos/id/306/200/300' },
159 | { href: '#!/four!', src: 'https://picsum.photos/id/304/200/300' },
160 | { href: '#!/five!', src: 'https://picsum.photos/id/305/200/300' },
161 | ] })`,
162 | }),
163 |
164 | m('h3.header', 'Pagination'),
165 | m(
166 | '.row',
167 | m(Pagination, {
168 | size: 5,
169 | curPage: curPage(),
170 | items: [
171 | { href: '/misc?page=1' },
172 | { href: '/misc?page=2' },
173 | { href: '/misc?page=3' },
174 | { href: '/misc?page=4' },
175 | { href: '/misc?page=5' },
176 | { href: '/misc?page=6' },
177 | { href: '/misc?page=7' },
178 | { href: '/misc?page=8' },
179 | { href: '/misc?page=9' },
180 | { href: '/misc?page=10' },
181 | { href: '/misc?page=11' },
182 | { href: '/misc?page=12' },
183 | ],
184 | })
185 | ),
186 | m(CodeBlock, {
187 | code: `m(Pagination, {
188 | size: 5,
189 | items: [
190 | { href: '/misc?page=1' },
191 | { href: '/misc?page=2' },
192 | { href: '/misc?page=3' },
193 | { href: '/misc?page=4' },
194 | { href: '/misc?page=5' },
195 | { href: '/misc?page=6' },
196 | { href: '/misc?page=7' },
197 | { href: '/misc?page=8' },
198 | { href: '/misc?page=9' },
199 | { href: '/misc?page=10' },
200 | { href: '/misc?page=11' },
201 | { href: '/misc?page=12' },
202 | ],
203 | })`,
204 | }),
205 | ]),
206 | };
207 | };
208 |
--------------------------------------------------------------------------------
/packages/example/src/components/modals/modal-page.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import {
3 | ModalPanel,
4 | CodeBlock,
5 | Button,
6 | MaterialBox,
7 | Select,
8 | ISelectOptions,
9 | Dropdown,
10 | IDropdownOptions,
11 | } from 'mithril-materialized';
12 | import gogh from '../../assets/Vincent_van_Gogh_-_Landscape_at_twilight_-_Google_Art_Project.jpg';
13 |
14 | export const ModalPage = () => {
15 | const onchange = (v: unknown) => alert(v);
16 | return {
17 | view: () =>
18 | m('.col.s12', [
19 | m('h2.header', 'Modals'),
20 | m('p', [
21 | 'The library supports all three modals types that are defined on the ',
22 | m('a[href=https://materializecss.com/modals.html#!][target=_blank]', 'materialize-css website'),
23 | '.',
24 | ]),
25 |
26 | m('h3.header', 'Normal Modal'),
27 | m(
28 | '.row',
29 | m(Button, { label: 'Open modal', modalId: 'modal1' }),
30 | m(ModalPanel, {
31 | id: 'modal1',
32 | title: 'Do you like this library?',
33 | description: 'This is some content.',
34 | options: { opacity: 0.7 },
35 | buttons: [
36 | {
37 | label: 'Disagree',
38 | onclick: () => onchange('You make me sad...'),
39 | },
40 | {
41 | label: 'Agree',
42 | onclick: () => onchange('Thank you for the compliment!'),
43 | },
44 | ],
45 | })
46 | ),
47 | m(CodeBlock, {
48 | code: ` m(Button, { label: 'Open modal', modalId: 'modal1' }),
49 | m(ModalPanel, {
50 | id: 'modal1',
51 | title: 'Do you like this library?',
52 | description: 'This is some content.',
53 | options: { opacity: 0.7 },
54 | buttons: [
55 | {
56 | label: 'Disagree',
57 | onclick: () => onchange('You make me sad...'),
58 | },
59 | {
60 | label: 'Agree',
61 | onclick: () => onchange('Thank you for the compliment!'),
62 | },
63 | ],
64 | })`,
65 | }),
66 |
67 | m('h3.header', 'Normal Modal with Select and Dropdown'),
68 | m(
69 | '.row',
70 | m(Button, { label: 'Open modal', modalId: 'modal1b' }),
71 | m(ModalPanel, {
72 | id: 'modal1b',
73 | title: 'Tell me about yourself',
74 | description: m(
75 | '.row', // So the content has enough vertical space
76 | [
77 | m(Select, {
78 | dropdownOptions: { container: document.body }, // So the select is not hidden
79 | iconName: 'person',
80 | label: 'What is your favorite hobby?',
81 | placeholder: 'Pick one',
82 | isMandatory: true,
83 | options: [
84 | { label: 'Pick one', disabled: true },
85 | { id: 'movies', label: 'Watching movies' },
86 | { id: 'out', label: 'Going out' },
87 | { id: 'sex', label: 'Having sex' },
88 | { id: 'fitness', label: 'Fitness' },
89 | { id: 'sleep', label: 'Sleeping' },
90 | ],
91 | onchange: (v) => console.log(v),
92 | } as ISelectOptions),
93 | m(Dropdown, {
94 | container: document.body, // So the dropdown is not hidden
95 | id: 'hobby',
96 | iconName: 'my_location',
97 | label: 'Pick a hobby',
98 | helperText: 'Help me',
99 | className: 'col s6',
100 | items: [
101 | { label: 'Movies', id: 'movies', iconName: 'local_movies' },
102 | { label: 'Reading', id: 'reading', iconName: 'import_contacts' },
103 | { label: 'Eating', id: 'eating', iconName: 'restaurant' },
104 | { label: '', divider: true },
105 | { label: 'Sex', id: 'sex', iconName: 'group' },
106 | ],
107 | onchange: (v) => console.log(v),
108 | } as IDropdownOptions),
109 | ]
110 | ),
111 |
112 | options: { opacity: 0.7 },
113 | buttons: [
114 | {
115 | label: 'Disagree',
116 | },
117 | {
118 | label: 'Agree',
119 | },
120 | ],
121 | })
122 | ),
123 | m(CodeBlock, {
124 | code: ` m(Button, { label: 'Open modal', modalId: 'modal1b' }),
125 | m(ModalPanel, {
126 | id: 'modal1b',
127 | title: 'Tell me about yourself',
128 | description: m(
129 | '.row', // So the content has enough vertical space
130 | [
131 | m(Select, {
132 | dropdownOptions: { container: document.body }, // So the select is not hidden
133 | iconName: 'person',
134 | label: 'What is your favorite hobby?',
135 | placeholder: 'Pick one',
136 | isMandatory: true,
137 | options: [
138 | { label: 'Pick one', disabled: true },
139 | { id: 'movies', label: 'Watching movies' },
140 | { id: 'out', label: 'Going out' },
141 | { id: 'sex', label: 'Having sex' },
142 | { id: 'fitness', label: 'Fitness' },
143 | { id: 'sleep', label: 'Sleeping' },
144 | ],
145 | onchange: v => console.log(v),
146 | } as ISelectOptions),
147 | m(Dropdown, {
148 | container: document.body, // So the dropdown is not hidden
149 | id: 'hobby',
150 | iconName: 'my_location',
151 | label: 'Pick a hobby',
152 | helperText: 'Help me',
153 | className: 'col s6',
154 | items: [
155 | { label: 'Movies', id: 'movies', iconName: 'local_movies' },
156 | { label: 'Reading', id: 'reading', iconName: 'import_contacts' },
157 | { label: 'Eating', id: 'eating', iconName: 'restaurant' },
158 | { label: '', divider: true },
159 | { label: 'Sex', id: 'sex', iconName: 'group' },
160 | ],
161 | onchange: v => console.log(v),
162 | } as IDropdownOptions),
163 | ]
164 | ),
165 | options: { opacity: 0.7 },
166 | buttons: [
167 | {
168 | label: 'Disagree',
169 | },
170 | {
171 | label: 'Agree',
172 | },
173 | ],
174 | })`,
175 | }),
176 |
177 | m('h3.header', 'Fixed Footer Modal'),
178 | m(
179 | '.row',
180 | m(Button, { label: 'Fixed footer modal', modalId: 'modal2' }),
181 | m(ModalPanel, {
182 | id: 'modal2',
183 | title: 'Do you like this library?',
184 | fixedFooter: true,
185 | richContent: true,
186 | description: `This is some content.
187 | This is some content.
188 | This is some content.
189 | This is some content.
190 | This is some content.
191 | This is some content.
192 | This is some content.
193 | This is some content.
194 | This is some content.
195 | This is some content.
196 | This is some content.
197 | This is some content.
198 | This is some content.
199 | This is some content.
200 | This is some content.
201 | This is some content.
202 | This is some content.
203 | This is some content.
204 | This is some content.
205 | This is some content.
206 | This is some content.
207 | This is some content.
208 | This is some content.
209 | This is some content.
210 | This is some content.
211 | This is some content.
212 | This is some content.
213 | This is some content.
214 | This is some content.
215 | This is some content.
216 | This is some content.
217 | `,
218 | buttons: [
219 | {
220 | label: 'Disagree',
221 | onclick: () => onchange('You make me sad...'),
222 | },
223 | {
224 | label: 'Agree',
225 | onclick: () => onchange('Thank you for the compliment!'),
226 | },
227 | ],
228 | })
229 | ),
230 | m(CodeBlock, {
231 | code: ` m(Button, { label: 'Bottom modal', modalId: 'modal3' }),
232 | m(ModalPanel, {
233 | id: 'modal3',
234 | title: 'Do you like this library?',
235 | description: 'This is some content. ... and much more',
236 | fixedFooter: true,
237 | richContent: true, // If richContent is true, it means that the description may contain HTML.
238 | buttons: [
239 | {
240 | label: 'Disagree',
241 | onclick: () => onchange('You make me sad...'),
242 | },
243 | {
244 | label: 'Agree',
245 | onclick: () => onchange('Thank you for the compliment!'),
246 | },
247 | ],
248 | })`,
249 | }),
250 |
251 | m('h3.header', 'Bottom Modal'),
252 | m(
253 | '.row',
254 | m(Button, { label: 'Open bottom modal', modalId: 'modal3' }),
255 | m(ModalPanel, {
256 | id: 'modal3',
257 | title: 'Do you like this library?',
258 | description: 'This is some content.',
259 | bottomSheet: true,
260 | buttons: [
261 | {
262 | label: 'Disagree',
263 | onclick: () => onchange('You make me sad...'),
264 | },
265 | {
266 | label: 'Agree',
267 | onclick: () => onchange('Thank you for the compliment!'),
268 | },
269 | ],
270 | })
271 | ),
272 | m(CodeBlock, {
273 | code: ` m(Button, { label: 'Open modal', modalId: 'modal1' }),
274 | m(ModalPanel, {
275 | id: 'modal1',
276 | title: 'Do you like this library?',
277 | description: 'This is some content.',
278 | bottomSheet: true,
279 | buttons: [
280 | {
281 | label: 'Disagree',
282 | onclick: () => onchange('You make me sad...'),
283 | },
284 | {
285 | label: 'Agree',
286 | onclick: () => onchange('Thank you for the compliment!'),
287 | },
288 | ],
289 | })`,
290 | }),
291 |
292 | m('h3.header', 'Vnode as content'),
293 | m(
294 | '.row',
295 | m(Button, { label: 'Open bottom content modal', modalId: 'modal4' }),
296 | m(ModalPanel, {
297 | id: 'modal4',
298 | title: 'Content modal',
299 | description: m(MaterialBox, { src: gogh, width: 400 }),
300 | bottomSheet: true,
301 | })
302 | ),
303 | m(CodeBlock, {
304 | code: ` m(Button, { label: 'Open bottom content modal', modalId: 'modal4' }),
305 | m(ModalPanel, {
306 | id: 'modal4',
307 | title: 'Content modal',
308 | description: m(MaterialBox, { src: gogh, width: 400 }),
309 | bottomSheet: true,
310 | })`,
311 | }),
312 | ]),
313 | };
314 | };
315 |
--------------------------------------------------------------------------------
/packages/example/src/components/pickers/picker-page.ts:
--------------------------------------------------------------------------------
1 | import { DatePicker, TimePicker, CodeBlock, Switch } from 'mithril-materialized';
2 | import m from 'mithril';
3 |
4 | export const PickerPage = () => {
5 | const state = { disabled: false };
6 |
7 | const onchange = (v: unknown) => alert(`Input changed. New value: ${v}`);
8 | return {
9 | view: () =>
10 | m('.col.s12', [
11 | m('h2.header', 'Pickers'),
12 | m(
13 | '.row',
14 | m(Switch, {
15 | label: 'Disable pickers',
16 | left: 'enable',
17 | right: 'disable',
18 | onchange: (v) => (state.disabled = v),
19 | })
20 | ),
21 | m('h3.header', 'DatePicker'),
22 | m(
23 | '.row',
24 | m(DatePicker, {
25 | disabled: state.disabled,
26 | format: 'mmmm d, yyyy',
27 | label: 'What is your birthday?',
28 | yearRange: [1970, new Date().getFullYear() + 20],
29 | initialValue: new Date(),
30 | onchange,
31 | })
32 | ),
33 | m(CodeBlock, {
34 | code: ` m(DatePicker, {
35 | format: 'mmmm d, yyyy',
36 | label: 'What is your birthday?',
37 | yearRange: [1970, new Date().getFullYear() + 20],
38 | initialValue: new Date().toDateString(),
39 | onchange,
40 | })`,
41 | }),
42 |
43 | m('h3.header', 'TimePicker'),
44 | m(
45 | '.row',
46 | m(TimePicker, {
47 | disabled: state.disabled,
48 | label: 'When do you normally get up?',
49 | twelveHour: false,
50 | initialValue: '09:00',
51 | onchange,
52 | })
53 | ),
54 | m(CodeBlock, {
55 | code: ` m(TimePicker, {
56 | label: 'What is your birthday?',
57 | twelveHour: false,
58 | initialValue: '09:00',
59 | onchange,
60 | })`,
61 | }),
62 | ]),
63 | };
64 | };
65 |
--------------------------------------------------------------------------------
/packages/example/src/components/timeline/timeline-page.ts:
--------------------------------------------------------------------------------
1 | import { CodeBlock, Timeline, padLeft, Collection, Icon } from 'mithril-materialized';
2 | import m from 'mithril';
3 |
4 | export const TimelinePage = () => {
5 | const timeFormatter = (d: Date) =>
6 | `${padLeft(d.getHours())}:${padLeft(d.getMinutes())}:${padLeft(d.getSeconds())}`;
7 |
8 | return {
9 | view: () =>
10 | m('.col.s12', [
11 | m('h2.header', 'Timeline'),
12 | m('p', [
13 | 'A simple timeline component based on ',
14 | m(
15 | 'a[href=https://tympanus.net/codrops/2013/05/02/vertical-timeline/][target=_blank]',
16 | `Codrops\' Vertical Timeline`
17 | ),
18 | '.',
19 | ]),
20 |
21 | m('h3.header', 'Timeline'),
22 | m(Timeline, {
23 | onSelect: (item: unknown) => console.table(item),
24 | timeFormatter,
25 | items: [
26 | {
27 | id: '1',
28 | title: 'Test a string',
29 | iconName: 'play_arrow',
30 | datetime: new Date(2019, 2, 3, 9, 0, 0),
31 | content: 'Hello world',
32 | },
33 | {
34 | id: '2',
35 | title: 'Test a long text',
36 | iconName: 'play_arrow',
37 | datetime: new Date(2019, 2, 3, 9, 30, 0),
38 | content: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus maximus erat,
39 | vitae placerat nisl blandit tincidunt. Vestibulum libero turpis, bibendum sit amet rutrum a,
40 | malesuada at diam. Praesent id dignissim ligula. Donec nec finibus lectus. Curabitur in
41 | sollicitudin sem. Nulla neque est, elementum et lectus ut, luctus elementum metus.`,
42 | },
43 | {
44 | id: '3',
45 | title: 'Test an active item',
46 | iconName: 'play_arrow',
47 | datetime: new Date(2019, 2, 3, 9, 45, 0),
48 | content: 'Hello world',
49 | active: true,
50 | },
51 | {
52 | id: '4',
53 | title: 'Test Vnode content',
54 | iconName: 'play_arrow',
55 | datetime: new Date(2019, 2, 3, 10, 5, 0),
56 | content: m(Collection, {
57 | style: 'color: black;',
58 | items: [
59 | { title: 'John', iconName: 'send' },
60 | { title: 'Mary', iconName: 'send' },
61 | { title: 'Pete', iconName: 'send' },
62 | ],
63 | }),
64 | },
65 | {
66 | id: '5',
67 | title: 'Test other icon',
68 | iconName: 'visibility',
69 | datetime: new Date(2019, 2, 3, 10, 11, 0),
70 | content: 'Hello world',
71 | },
72 | {
73 | id: '6',
74 | iconName: 'visibility_off',
75 | datetime: new Date(2019, 2, 3, 10, 15, 0),
76 | content: 'No title, only content',
77 | },
78 | {
79 | id: '7',
80 | title: m('i', [
81 | 'Test a Vnode',
82 | m(Icon, { className: 'small', style: 'float: right;', iconName: 'directions_run' }),
83 | ]),
84 | iconName: 'visibility',
85 | datetime: new Date(2019, 2, 3, 10, 21, 0),
86 | },
87 | ],
88 | }),
89 | m(CodeBlock, {
90 | code: ` m(Timeline, {
91 | onSelect: (item: ITimelineItem) => console.table(item),
92 | timeFormatter, // Adds seconds to time format
93 | items: [
94 | {
95 | title: 'Test a string',
96 | iconName: 'play_arrow',
97 | datetime: new Date(2019, 2, 3, 9, 0, 0),
98 | content: 'Hello world',
99 | },
100 | {
101 | title: 'Test a long text',
102 | iconName: 'play_arrow',
103 | datetime: new Date(2019, 2, 3, 9, 30, 0),
104 | content: 'Lorem ipsum ...',
105 | },
106 | {
107 | title: 'Test an active item',
108 | iconName: 'play_arrow',
109 | datetime: new Date(2019, 2, 3, 9, 45, 0),
110 | content: 'Hello world',
111 | active: true,
112 | },
113 | {
114 | title: 'Test Vnode content',
115 | iconName: 'play_arrow',
116 | datetime: new Date(2019, 2, 3, 10, 5, 0),
117 | content: m(Collection, {
118 | style: 'color: black;', // otherwise the titles are in white
119 | items: [
120 | { title: 'John', iconName: 'send' },
121 | { title: 'Mary', iconName: 'send' },
122 | { title: 'Pete', iconName: 'send' },
123 | ],
124 | }),
125 | },
126 | {
127 | title: 'Test other icon',
128 | iconName: 'visibility',
129 | datetime: new Date(2019, 2, 3, 10, 11, 0),
130 | content: 'Hello world',
131 | },
132 | {
133 | id: '6',
134 | iconName: 'visibility_off',
135 | datetime: new Date(2019, 2, 3, 10, 15, 0),
136 | content: 'No title, only content',
137 | },
138 | {
139 | id: '7',
140 | title: m('i', [
141 | 'Test a Vnode',
142 | m(Icon, { className: 'small', style: 'float: right;', iconName: 'directions_run' }),
143 | ]),
144 | iconName: 'visibility',
145 | datetime: new Date(2019, 2, 3, 10, 21, 0),
146 | },
147 | ],
148 | })`,
149 | }),
150 | ]),
151 | };
152 | };
153 |
--------------------------------------------------------------------------------
/packages/example/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erikvullings/mithril-materialized/f08c3fa6784bc8f2042df97570ef525e11f68a54/packages/example/src/favicon.ico
--------------------------------------------------------------------------------
/packages/example/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Mithril-Materialized
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/example/src/models/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { ComponentTypes } from 'mithril';
2 |
3 | export interface IDashboard {
4 | id: string;
5 | default?: boolean;
6 | title: string;
7 | icon?: string;
8 | route: string;
9 | visible: boolean;
10 | component: ComponentTypes;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/example/src/services/dashboard-service.ts:
--------------------------------------------------------------------------------
1 | import m, { ComponentTypes, RouteDefs } from 'mithril';
2 | import { IDashboard } from '../models/dashboard';
3 | import { Layout } from '../components/layout';
4 | import { HomePage } from '../components/home/home-page';
5 | import { AboutPage } from '../components/about/about-page';
6 | import { ButtonPage } from '../components/buttons/button-page';
7 | import { InputPage } from '../components/inputs/input-page';
8 | import { PickerPage } from '../components/pickers/picker-page';
9 | import { SelectionPage } from '../components/selections/selection-page';
10 | import { ModalPage } from '../components/modals/modal-page';
11 | import { MiscPage } from '../components/misc/misc-page';
12 | import { CollectionsPage } from '../components/collections/collections-page';
13 | import { MapEditorPage } from './../components/map-editor/map-editor-page';
14 | import { TimelinePage } from '../components/timeline/timeline-page';
15 |
16 | export enum Dashboards {
17 | HOME = 'HOME',
18 | BUTTONS = 'BUTTONS',
19 | INPUTS = 'INPUTS',
20 | PICKERS = 'PICKERS',
21 | SELECTIONS = 'SELECTIONS',
22 | COLLECTIONS = 'COLLECTIONS',
23 | MAP_EDITOR = 'MAP_EDITOR',
24 | MODALS = 'MODALS',
25 | TIMELINE = 'TIMELINE',
26 | KANBAN = 'KANBAN',
27 | MISC = 'MISC',
28 | ABOUT = 'ABOUT',
29 | }
30 |
31 | class DashboardService {
32 | private dashboards!: ReadonlyArray;
33 |
34 | constructor(private layout: ComponentTypes, dashboards: IDashboard[]) {
35 | this.setList(dashboards);
36 | }
37 |
38 | public getList() {
39 | return this.dashboards;
40 | }
41 |
42 | public setList(list: IDashboard[]) {
43 | this.dashboards = Object.freeze(list);
44 | }
45 |
46 | public get defaultRoute() {
47 | const dashboard = this.dashboards.filter((d) => d.default).shift();
48 | return dashboard ? dashboard.route : this.dashboards[0].route;
49 | }
50 |
51 | public switchTo(dashboardId: Dashboards, fragment = '') {
52 | const dashboard = this.dashboards.filter((d) => d.id === dashboardId).shift();
53 | if (dashboard) {
54 | m.route.set(dashboard.route);
55 | }
56 | }
57 |
58 | public get routingTable() {
59 | return this.dashboards.reduce((p, c) => {
60 | p[c.route] = { render: () => m(this.layout, m(c.component)) };
61 | return p;
62 | }, {} as RouteDefs);
63 | }
64 | }
65 |
66 | export const dashboardSvc: DashboardService = new DashboardService(Layout, [
67 | {
68 | id: Dashboards.HOME,
69 | default: true,
70 | title: 'HOME',
71 | icon: 'home',
72 | route: '/home',
73 | visible: true,
74 | component: HomePage,
75 | },
76 | {
77 | id: Dashboards.BUTTONS,
78 | title: 'BUTTONS',
79 | icon: 'crop_16_9',
80 | route: '/buttons',
81 | visible: true,
82 | component: ButtonPage,
83 | },
84 | {
85 | id: Dashboards.INPUTS,
86 | title: 'INPUTS',
87 | icon: 'create',
88 | route: '/inputs',
89 | visible: true,
90 | component: InputPage,
91 | },
92 | {
93 | id: Dashboards.PICKERS,
94 | title: 'PICKERS',
95 | icon: 'access_time',
96 | route: '/pickers',
97 | visible: true,
98 | component: PickerPage,
99 | },
100 | {
101 | id: Dashboards.SELECTIONS,
102 | title: 'SELECTIONS',
103 | icon: 'check',
104 | route: '/selections',
105 | visible: true,
106 | component: SelectionPage,
107 | },
108 | {
109 | id: Dashboards.MODALS,
110 | title: 'MODALS',
111 | icon: 'all_out',
112 | route: '/modals',
113 | visible: true,
114 | component: ModalPage,
115 | },
116 | {
117 | id: Dashboards.COLLECTIONS,
118 | title: 'COLLECTIONS',
119 | icon: 'collections',
120 | route: '/collections',
121 | visible: true,
122 | component: CollectionsPage,
123 | },
124 | {
125 | id: Dashboards.MAP_EDITOR,
126 | title: 'MAP-EDITOR',
127 | icon: 'playlist_add',
128 | route: '/map_editor',
129 | visible: true,
130 | component: MapEditorPage,
131 | },
132 | {
133 | id: Dashboards.TIMELINE,
134 | title: 'TIMELINE',
135 | icon: 'timeline',
136 | route: '/timeline',
137 | visible: true,
138 | component: TimelinePage,
139 | },
140 | {
141 | id: Dashboards.MISC,
142 | title: 'MISCELLANEOUS',
143 | icon: 'image',
144 | route: '/misc',
145 | visible: true,
146 | component: MiscPage,
147 | },
148 | {
149 | id: Dashboards.ABOUT,
150 | title: 'ABOUT',
151 | icon: 'info',
152 | route: '/about',
153 | visible: true,
154 | component: AboutPage,
155 | },
156 | ]);
157 |
--------------------------------------------------------------------------------
/packages/example/src/utils/jpg.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.jpg" {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/example/src/utils/json.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.json" {
2 | const content: any;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/example/src/utils/png.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png" {
2 | const content: any;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/example/src/utils/svg.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svg" {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/example/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a GUID
3 | * @see https://stackoverflow.com/a/2117523/319711
4 | *
5 | * @returns RFC4122 version 4 compliant GUID
6 | */
7 | export const uuid4 = () => {
8 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
9 | // tslint:disable-next-line:no-bitwise
10 | const r = (Math.random() * 16) | 0;
11 | // tslint:disable-next-line:no-bitwise
12 | const v = c === 'x' ? r : (r & 0x3) | 0x8;
13 | return v.toString(16);
14 | });
15 | };
16 |
17 | /**
18 | * Create a unique ID
19 | * @see https://stackoverflow.com/a/2117523/319711
20 | *
21 | * @returns RFC4122 version 4 compliant GUID
22 | */
23 | export const uniqueId = () => {
24 | return 'idxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
25 | // tslint:disable-next-line:no-bitwise
26 | const r = (Math.random() * 16) | 0;
27 | // tslint:disable-next-line:no-bitwise
28 | const v = c === 'x' ? r : (r & 0x3) | 0x8;
29 | return v.toString(16);
30 | });
31 | };
32 |
33 | export const toLetters = (num: number): string => {
34 | const mod = num % 26;
35 | // tslint:disable-next-line:no-bitwise
36 | let pow = (num / 26) | 0;
37 | const out = mod ? String.fromCharCode(64 + mod) : (--pow, 'Z');
38 | return pow ? toLetters(pow) + out : out;
39 | };
40 |
41 | /**
42 | * Generate a sequence of numbers between from and to with step size: [from, to].
43 | *
44 | * @static
45 | * @param {number} from
46 | * @param {number} to : inclusive
47 | * @param {number} [count=to-from+1]
48 | * @param {number} [step=1]
49 | * @returns
50 | */
51 | export const range = (from: number, to: number, count: number = to - from + 1, step: number = 1) => {
52 | // See here: http://stackoverflow.com/questions/3746725/create-a-javascript-array-containing-1-n
53 | // let a = Array.apply(null, {length: n}).map(Function.call, Math.random);
54 | const a: number[] = new Array(count);
55 | const min = from;
56 | const max = to - (count - 1) * step;
57 | const theRange = max - min;
58 | const x0 = Math.round(from + theRange * Math.random());
59 | for (let i = 0; i < count; i++) {
60 | a[i] = x0 + i * step;
61 | }
62 | return a;
63 | };
64 |
65 | /**
66 | * Deep copy function for TypeScript.
67 | * @param T Generic type of target/copied value.
68 | * @param target Target value to be copied.
69 | * @see Source project, ts-deepcopy https://github.com/ykdr2017/ts-deepcopy
70 | * @see Code pen https://codepen.io/erikvullings/pen/ejyBYg
71 | */
72 | export const deepCopy = (target: T): T => {
73 | if (target === null) {
74 | return target;
75 | }
76 | if (target instanceof Date) {
77 | return new Date(target.getTime()) as any;
78 | }
79 | if (target instanceof Array) {
80 | const cpy = [] as any[];
81 | (target as any[]).forEach((v) => {
82 | cpy.push(v);
83 | });
84 | return cpy.map((n: any) => deepCopy(n)) as any;
85 | }
86 | if (typeof target === 'object') {
87 | const cpy = { ...(target as { [key: string]: any }) } as {
88 | [key: string]: any;
89 | };
90 | Object.keys(cpy).forEach((k) => {
91 | cpy[k] = deepCopy(cpy[k]);
92 | });
93 | return cpy as T;
94 | }
95 | return target;
96 | };
97 |
98 | /**
99 | * Function to filter case-insensitive title and description.
100 | * @param filterValue Filter text
101 | */
102 | export const titleAndDescriptionFilter = (filterValue: string) => {
103 | filterValue = filterValue.toLowerCase();
104 | return (content: { title: string; description: string }) =>
105 | !filterValue ||
106 | !content.title ||
107 | content.title.toLowerCase().indexOf(filterValue) >= 0 ||
108 | (content.description && content.description.toLowerCase().indexOf(filterValue) >= 0);
109 | };
110 |
111 | /**
112 | * Convert strings like XmlHTTPRequest to Xml HTTP Request
113 | * @see https://stackoverflow.com/a/6229124/319711
114 | */
115 | export const unCamelCase = (str?: string) =>
116 | str
117 | ? str
118 | .replace(/([a-z])([A-Z])/g, '$1 $2') // insert a space between lower & upper
119 | .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3') // space before last upper in a sequence followed by lower
120 | .replace(/^./, (char) => char.toUpperCase()) // uppercase the first character
121 | : '';
122 |
123 | export const deepEqual = (x?: T, y?: T): boolean => {
124 | const tx = typeof x;
125 | const ty = typeof y;
126 | return x && y && tx === 'object' && tx === ty
127 | ? Object.keys(x).length === Object.keys(y).length && Object.keys(x).every((key) => deepEqual(x[key], y[key]))
128 | : x === y;
129 | };
130 |
131 | // let i = 0;
132 | // console.log(`${++i}: ${deepEqual([1, 2, 3], [1, 2, 3])}`);
133 | // console.log(`${++i}: ${deepEqual([1, 2, 3], [1, 2, 3, 4])}`);
134 | // console.log(`${++i}: ${deepEqual({ a: 'foo', b: 'bar' }, { a: 'foo', b: 'bar' })}`);
135 | // console.log(`${++i}: ${deepEqual({ a: 'foo', b: 'bar' }, { b: 'bar', a: 'foo' })}`);
136 |
137 | /** Remove paragraphs and
and the beginning and end of a string. */
138 | export const removeParagraphs = (s: string) => s.replace(/<\/?p>/g, '');
139 |
140 | export const removeHtml = (s: string) => s.replace(/<\/?[0-9a-zA-Z=\[\]_ \-"]+>/gm, '').replace(/"/gi, '"');
141 |
--------------------------------------------------------------------------------
/packages/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
5 | "module": "NodeNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6 | "lib": [
7 | "dom",
8 | "es5",
9 | "es2015.promise",
10 | "es2017"
11 | ] /* Specify library files to be included in the compilation. */,
12 | "forceConsistentCasingInFileNames": true,
13 | // "allowJs": true, /* Allow javascript files to be compiled. */
14 | // "checkJs": true, /* Report errors in .js files. */
15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
17 | "sourceMap": true /* Generates corresponding '.map' file. */,
18 | // "outFile": "./", /* Concatenate and emit output to single file. */
19 | "outDir": "./dist" /* Redirect output structure to the directory. */,
20 | // "rootDir":
21 | // "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
22 | // "removeComments": true, /* Do not emit comments to output. */
23 | // "noEmit": true, /* Do not emit outputs. */
24 | "importHelpers": true /* Import emit helpers from 'tslib'. */,
25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
27 | /* Strict Type-Checking Options */
28 | "strict": true /* Enable all strict type-checking options. */,
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 | /* Module Resolution Options */
41 | "moduleResolution": "nodenext", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
45 | // "typeRoots": [], /* List of folders to include type definitions from. */
46 | // "types": [], /* Type declaration files to be included in compilation. */
47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
48 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
50 | /* Source Map Options */
51 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
52 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
55 | /* Experimental Options */
56 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
57 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
58 | }
59 | }
--------------------------------------------------------------------------------
/packages/example/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended"],
4 | "jsRules": {},
5 | "rules": {
6 | "no-console": false,
7 | "no-debugger": false,
8 | "quotemark": [true, "single"],
9 | "trailing-comma": [
10 | true,
11 | {
12 | "multiline": {
13 | "objects": "always",
14 | "arrays": "always",
15 | "functions": "never",
16 | "typeLiterals": "ignore"
17 | },
18 | "esSpecCompliant": true
19 | }
20 | ],
21 | "object-literal-sort-keys": false,
22 | "ordered-imports": false,
23 | "arrow-parens": [false, "ban-single-arg-parens"]
24 | },
25 | "rulesDirectory": []
26 | }
27 |
--------------------------------------------------------------------------------
/packages/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const Dotenv = require('dotenv-webpack');
4 | const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin');
5 | // const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
7 |
8 | module.exports = (env) => {
9 | const isProduction = env.production;
10 | const outputPath = path.resolve(__dirname, isProduction ? '../../docs' : 'dist');
11 | const publicPath = isProduction ? 'https://erikvullings.github.io/mithril-materialized/' : '/';
12 |
13 | console.log(`Running in ${isProduction ? 'production' : 'development'} mode, output directed to ${outputPath}.`);
14 |
15 | return {
16 | mode: isProduction ? 'production' : 'development',
17 | entry: './src/app.ts',
18 | devtool: isProduction ? 'source-map' : 'inline-source-map',
19 | devServer: {
20 | liveReload: true,
21 | port: 1235,
22 | },
23 | plugins: [
24 | new Dotenv(),
25 | new HtmlWebpackPlugin({
26 | title: 'Mithril Materialized',
27 | favicon: './src/favicon.ico',
28 | meta: { viewport: 'width=device-width, initial-scale=1' },
29 | }),
30 | new HtmlWebpackTagsPlugin({
31 | metas: [
32 | {
33 | attributes: { property: 'og:title', content: 'Mithril Materialized' },
34 | },
35 | {
36 | attributes: {
37 | property: 'og:description',
38 | content: 'A materialize-css library for Mithril.',
39 | },
40 | },
41 | {
42 | attributes: {
43 | property: 'og:url',
44 | content: 'https://erikvullings.github.io/mithril-materialized',
45 | },
46 | },
47 | {
48 | path: './src/assets/favicon-32x32.png',
49 | attributes: {
50 | property: 'og:image',
51 | },
52 | },
53 | {
54 | attributes: { property: 'og:locale', content: 'en_UK' },
55 | },
56 | {
57 | attributes: { property: 'og:site_name', content: 'Mithril Materialized' },
58 | },
59 | {
60 | attributes: { property: 'og:image:alt', content: 'Mithril Materialized' },
61 | },
62 | {
63 | attributes: {
64 | property: 'og:image:type',
65 | content: 'image/png',
66 | },
67 | },
68 | {
69 | attributes: {
70 | property: 'og:image:width',
71 | content: '32',
72 | },
73 | },
74 | {
75 | attributes: {
76 | property: 'og:image:height',
77 | content: '32',
78 | },
79 | },
80 | ],
81 | }),
82 | // new MiniCssExtractPlugin({
83 | // filename: isProduction ? '[name].[contenthash].css' : '[name].css',
84 | // chunkFilename: isProduction ? '[id].[contenthash].css' : '[id].css',
85 | // }),
86 | ],
87 | module: {
88 | rules: [
89 | {
90 | test: /\.ts$/,
91 | use: 'ts-loader',
92 | exclude: /node_modules/,
93 | },
94 | {
95 | test: /\.css$/i,
96 | use: ['style-loader', 'css-loader'],
97 | },
98 | {
99 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
100 | type: 'asset/resource',
101 | },
102 | {
103 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
104 | type: 'asset/resource',
105 | },
106 | // {
107 | // test: /\.css$/,
108 | // use: [MiniCssExtractPlugin.loader, 'css-loader'],
109 | // },
110 | ],
111 | },
112 | resolve: {
113 | alias: {
114 | // 'materialize-css': path.resolve(__dirname, 'node_modules/materialize-css'),
115 | mithril: path.resolve(__dirname, 'node_modules/mithril'),
116 | },
117 | extensions: ['.ts', '.js'],
118 | },
119 | optimization: {
120 | minimizer: [
121 | // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
122 | // `...`,
123 | new CssMinimizerPlugin(),
124 | ],
125 | },
126 | output: {
127 | filename: 'bundle.js',
128 | path: outputPath,
129 | publicPath,
130 | },
131 | };
132 | };
133 |
--------------------------------------------------------------------------------
/packages/lib/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .rpt2_cache
--------------------------------------------------------------------------------
/packages/lib/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | tsconfig.json
3 | tslint.json
4 | shrinkwrap.yaml
5 | pnpm-lock.yaml
6 | src
7 | .rpt2_cache
8 | rollup.config.js
--------------------------------------------------------------------------------
/packages/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-materialized",
3 | "version": "1.4.2",
4 | "description": "A materialize library for mithril.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.esm.js",
7 | "browser": "dist/index.umd.js",
8 | "unpkg": "dist/index.umd.js",
9 | "jsdelivr": "dist/index.umd.js",
10 | "types": "dist/index.d.ts",
11 | "exports": {
12 | ".": {
13 | "types": "./dist/index.d.ts",
14 | "import": "./dist/index.modern.js",
15 | "require": "./dist/index.js",
16 | "default": "./dist/index.modern.js"
17 | },
18 | "./modern": "./dist/index.modern.js",
19 | "./esm": "./dist/index.esm.js",
20 | "./umd": "./dist/index.umd.js",
21 | "./index.css": "./dist/index.css"
22 | },
23 | "sideEffects": false,
24 | "files": [
25 | "dist"
26 | ],
27 | "scripts": {
28 | "build": "microbundle build ./src/index.ts --external mithril,materialize-css",
29 | "dev": "microbundle watch ./src/index.ts --external mithril,materialize-css",
30 | "start": "npm run dev",
31 | "clean": "rimraf dist node_modules/.cache",
32 | "link:old": "pnpm link",
33 | "typedoc": "typedoc --out ../../docs/typedoc src",
34 | "build:domain": "npm run clean && npm run build && typedoc --out ../../docs/typedoc src",
35 | "dry-run": "npm publish --dry-run",
36 | "patch-release": "npm run clean && npm run build && npm version patch --force -m \"Patch release\" && npm publish && git push --follow-tags",
37 | "minor-release": "npm run clean && npm run build && npm version minor --force -m \"Minor release\" && npm publish && git push --follow-tags",
38 | "major-release": "npm run clean && npm run build && npm version major --force -m \"Major release\" && npm publish && git push --follow-tags"
39 | },
40 | "repository": {
41 | "type": "git",
42 | "url": "git://github.com/erikvullings/mithril-materialized.git"
43 | },
44 | "keywords": [
45 | "mithril",
46 | "materialize-css"
47 | ],
48 | "author": "Erik Vullings (http://www.tno.nl)",
49 | "license": "MIT",
50 | "dependencies": {
51 | "materialize-css": "^1.0.0",
52 | "mithril": "^2.3.0"
53 | },
54 | "devDependencies": {
55 | "@types/materialize-css": "^1.0.14",
56 | "@types/mithril": "^2.2.7",
57 | "js-yaml": "^4.1.0",
58 | "microbundle": "^0.15.1",
59 | "rimraf": "^6.0.1",
60 | "tslib": "^2.8.1",
61 | "typedoc": "^0.28.4",
62 | "typescript": "^5.8.3"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/packages/lib/src/autocomplete.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent } from 'mithril';
2 | import { uniqueId } from './utils';
3 | import { IInputOptions } from './input-options';
4 | import { Label, HelperText } from './label';
5 |
6 | export interface IAutoCompleteOptions extends Partial, IInputOptions {}
7 |
8 | /** Component to auto complete your text input */
9 | export const Autocomplete: FactoryComponent = () => {
10 | const state = { id: uniqueId() };
11 | return {
12 | view: ({ attrs }) => {
13 | const id = attrs.id || state.id;
14 | // const attributes = toAttrs(attrs);
15 | const {
16 | label,
17 | helperText,
18 | initialValue,
19 | onchange,
20 | newRow,
21 | className = 'col s12',
22 | style,
23 | iconName,
24 | isMandatory,
25 | ...params
26 | } = attrs;
27 | const cn = newRow ? className + ' clear' : className;
28 | return m(`.input-field${newRow ? '.clear' : ''}`, { className: cn, style }, [
29 | iconName ? m('i.material-icons.prefix', iconName) : '',
30 | m('input', {
31 | ...params,
32 | className: 'autocomplete',
33 | type: 'text',
34 | tabindex: 0,
35 | id,
36 | oncreate: ({ dom }) => {
37 | M.Autocomplete.init(dom, attrs);
38 | },
39 | onchange: onchange
40 | ? (e: Event) => {
41 | if (e.target && (e.target as HTMLInputElement).value) {
42 | onchange((e.target as HTMLInputElement).value);
43 | }
44 | }
45 | : undefined,
46 | value: initialValue,
47 | }),
48 | m(Label, { label, id, isMandatory, isActive: initialValue }),
49 | m(HelperText, { helperText }),
50 | ]);
51 | },
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/packages/lib/src/button.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes } from 'mithril';
2 | import { Icon } from './icon';
3 |
4 | export interface IHtmlAttributes {
5 | id?: string;
6 | for?: string;
7 | placeholder?: string;
8 | autofocus?: boolean;
9 | disabled?: boolean;
10 | type?: 'submit' | 'button' | 'text' | 'textarea' | 'number';
11 | }
12 |
13 | export interface IMaterialButton extends Attributes {
14 | /** Optional (e.g. in case you only want to use an icon) button label */
15 | label?: string;
16 | /** Optional icon material-icons name, @see https://materializecss.com/icons.html */
17 | iconName?: string;
18 | /** Optional icon class, e.g. tiny (1em), small (2em), medium (4em), large (6em), or 'tiny right' */
19 | iconClass?: string;
20 | /**
21 | * If the button is intended to open a modal, specify its modal id so we can trigger it,
22 | * @see https://materializecss.com/modals.html
23 | */
24 | modalId?: string;
25 | /** Some additional HTML attributes that can be attached to the button */
26 | attr?: IHtmlAttributes;
27 | /** Optional text-based tooltip, @see https://materializecss.com/tooltips.html */
28 | tooltip?: string;
29 | /** Optional location for the tooltip */
30 | tooltipPostion?: 'top' | 'bottom' | 'left' | 'right';
31 | }
32 |
33 | /**
34 | * A factory to create new buttons.
35 | *
36 | * @example FlatButton = ButtonFactory('a.waves-effect.waves-teal.btn-flat');
37 | */
38 | export const ButtonFactory = (
39 | element: string,
40 | defaultClassNames: string,
41 | type: string = ''
42 | ): FactoryComponent => {
43 | return () => {
44 | return {
45 | view: ({ attrs }) => {
46 | const { modalId, tooltip, tooltipPostion, iconName, iconClass, label, className, attr, ...params } = attrs;
47 | const cn = [modalId ? 'modal-trigger' : '', tooltip ? 'tooltipped' : '', defaultClassNames, className]
48 | .filter(Boolean)
49 | .join(' ')
50 | .trim();
51 | return m(
52 | element,
53 | {
54 | ...params,
55 | ...attr,
56 | className: cn,
57 | href: modalId ? `#${modalId}` : undefined,
58 | 'data-position': tooltip ? tooltipPostion || 'top' : undefined,
59 | 'data-tooltip': tooltip || undefined,
60 | type,
61 | },
62 | // `${dca}${modalId ? `.modal-trigger[href=#${modalId}]` : ''}${
63 | // tooltip ? `.tooltipped[data-position=${tooltipPostion || 'top'}][data-tooltip=${tooltip}]` : ''
64 | // }${toAttributeString(attr)}`, {}
65 | iconName ? m(Icon, { iconName, className: iconClass || 'left' }) : undefined,
66 | label ? label : undefined
67 | );
68 | },
69 | };
70 | };
71 | };
72 |
73 | export const Button = ButtonFactory('a', 'waves-effect waves-light btn', 'button');
74 | export const LargeButton = ButtonFactory('a', 'waves-effect waves-light btn-large', 'button');
75 | export const SmallButton = ButtonFactory('a', 'waves-effect waves-light btn-small', 'button');
76 | export const FlatButton = ButtonFactory('a', 'waves-effect waves-teal btn-flat', 'button');
77 | export const RoundIconButton = ButtonFactory('button', 'btn-floating btn-large waves-effect waves-light', 'button');
78 | export const SubmitButton = ButtonFactory('button', 'btn waves-effect waves-light', 'submit');
79 |
--------------------------------------------------------------------------------
/packages/lib/src/carousel.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes } from 'mithril';
2 |
3 | export interface ICarouselItem extends Attributes {
4 | /** Relative page link, e.g. '#one' */
5 | href: string;
6 | /** Image source */
7 | src: string;
8 | /** Alternative name */
9 | alt?: string;
10 | }
11 |
12 | export interface ICarousel extends Partial, Attributes {
13 | /** The list of images */
14 | items: ICarouselItem[];
15 | }
16 |
17 | export const CarouselItem: FactoryComponent = () => {
18 | return {
19 | view: ({ attrs: { href, src, alt, ...params } }) => {
20 | return m('a.carousel-item', { ...params, href }, m('img', { src, alt }));
21 | },
22 | };
23 | };
24 |
25 | /**
26 | * Creates a collabsible or accordion (via the accordion option, default true) component.
27 | * @see https://materializecss.com/carousel.html
28 | */
29 | export const Carousel: FactoryComponent = () => {
30 | return {
31 | view: ({ attrs }) => {
32 | const { items } = attrs;
33 | return items && items.length > 0
34 | ? m(
35 | '.carousel',
36 | {
37 | oncreate: ({ dom }) => {
38 | M.Carousel.init(dom, attrs);
39 | },
40 | },
41 | items.map((item) => m(CarouselItem, item))
42 | )
43 | : undefined;
44 | },
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/packages/lib/src/chip.ts:
--------------------------------------------------------------------------------
1 | import m from 'mithril';
2 | import { uniqueId } from './utils';
3 | import { HelperText, Label } from './label';
4 |
5 | export interface ChipData {
6 | tag: string;
7 | image?: string;
8 | alt?: string;
9 | }
10 |
11 | export interface AutocompleteOption extends ChipData {
12 | value?: string;
13 | }
14 |
15 | export interface IChipsOptions {
16 | id?: string;
17 | data?: ChipData[];
18 | placeholder?: string;
19 | secondaryPlaceholder?: string;
20 | autocompleteOptions?: {
21 | data: Record | AutocompleteOption[];
22 | limit?: number;
23 | minLength?: number;
24 | };
25 | limit?: number;
26 | required?: boolean;
27 | isMandatory?: boolean;
28 | className?: string;
29 | label?: string;
30 | helperText?: string;
31 | onchange?: (data: ChipData[]) => void;
32 | onChipAdd?: (chip: ChipData) => void;
33 | onChipDelete?: (chip: ChipData) => void;
34 | onChipSelect?: (chip: ChipData) => void;
35 | }
36 |
37 | export const Chips: m.FactoryComponent = () => {
38 | interface ChipsState {
39 | chipsData: ChipData[];
40 | selectedChip: number | null;
41 | focused: boolean;
42 | inputValue: string;
43 | inputId: string;
44 | autocompleteItems: AutocompleteOption[];
45 | selectedAutocompleteIndex: number;
46 | showAutocomplete: boolean;
47 | }
48 |
49 | const state: ChipsState = {
50 | chipsData: [],
51 | selectedChip: null,
52 | focused: false,
53 | inputValue: '',
54 | inputId: uniqueId(),
55 | autocompleteItems: [],
56 | selectedAutocompleteIndex: -1,
57 | showAutocomplete: false,
58 | };
59 |
60 | let currentVnode: m.VnodeDOM | null = null;
61 |
62 | const processAutocompleteData = (
63 | data: Record | AutocompleteOption[]
64 | ): AutocompleteOption[] => {
65 | if (Array.isArray(data)) {
66 | return data.map((item) => {
67 | if (typeof item === 'string') {
68 | return { tag: item };
69 | }
70 | return item;
71 | });
72 | }
73 | return Object.entries(data).map(([text, value]) => ({
74 | tag: text,
75 | value: value || text,
76 | }));
77 | };
78 |
79 | const updateAutocomplete = () => {
80 | if (!currentVnode?.attrs.autocompleteOptions?.data) {
81 | state.autocompleteItems = [];
82 | return;
83 | }
84 |
85 | const { data, minLength = 1 } = currentVnode.attrs.autocompleteOptions;
86 | const input = state.inputValue.toLowerCase();
87 |
88 | if (input.length < minLength) {
89 | state.autocompleteItems = [];
90 | state.showAutocomplete = false;
91 | return;
92 | }
93 |
94 | const allOptions = processAutocompleteData(data);
95 | const filtered = allOptions.filter((option) => option.tag.toLowerCase().includes(input));
96 |
97 | const limit = currentVnode.attrs.autocompleteOptions.limit || Infinity;
98 | state.autocompleteItems = filtered.slice(0, limit);
99 | state.showAutocomplete = state.autocompleteItems.length > 0;
100 | state.selectedAutocompleteIndex = -1;
101 | };
102 |
103 | const selectAutocompleteItem = (item: AutocompleteOption) => {
104 | addChip({
105 | tag: item.tag,
106 | image: item.image,
107 | alt: item.alt, // Preserve alt text when converting to chip
108 | });
109 | state.inputValue = '';
110 | state.showAutocomplete = false;
111 | state.selectedAutocompleteIndex = -1;
112 | };
113 |
114 | const isValid = (chip: ChipData, currentChips: ChipData[]): boolean => {
115 | if (!chip.tag || chip.tag.trim() === '') return false;
116 | return !currentChips.some((c) => c.tag === chip.tag);
117 | };
118 |
119 | const addChip = (chip: ChipData) => {
120 | if (!currentVnode) return;
121 |
122 | const { limit = Infinity, onChipAdd, onchange } = currentVnode.attrs;
123 |
124 | if (!isValid(chip, state.chipsData) || state.chipsData.length >= limit) {
125 | return;
126 | }
127 |
128 | state.chipsData = [...state.chipsData, chip];
129 | state.inputValue = '';
130 |
131 | if (onChipAdd) onChipAdd(chip);
132 | if (onchange) onchange(state.chipsData);
133 | };
134 |
135 | const deleteChip = (index: number) => {
136 | if (!currentVnode) return;
137 |
138 | const { onChipDelete, onchange } = currentVnode.attrs;
139 | const chip = state.chipsData[index];
140 |
141 | state.chipsData = state.chipsData.filter((_, i) => i !== index);
142 | state.selectedChip = null;
143 |
144 | if (onChipDelete) onChipDelete(chip);
145 | if (onchange) onchange(state.chipsData);
146 | };
147 |
148 | const selectChip = (index: number) => {
149 | if (!currentVnode) return;
150 |
151 | const { onChipSelect } = currentVnode.attrs;
152 | state.selectedChip = index;
153 |
154 | if (onChipSelect && state.chipsData[index]) {
155 | onChipSelect(state.chipsData[index]);
156 | }
157 | };
158 |
159 | const handleKeydown = (e: KeyboardEvent) => {
160 | const target = e.target as HTMLInputElement;
161 |
162 | if (state.showAutocomplete) {
163 | if (e.key === 'ArrowDown') {
164 | e.preventDefault();
165 | state.selectedAutocompleteIndex = Math.min(
166 | state.selectedAutocompleteIndex + 1,
167 | state.autocompleteItems.length - 1
168 | );
169 | const selectedItem = currentVnode?.dom.querySelector('.autocomplete-item.selected') as HTMLElement;
170 | if (selectedItem) {
171 | selectedItem.scrollIntoView({ block: 'nearest' });
172 | }
173 | m.redraw();
174 | return;
175 | }
176 |
177 | if (e.key === 'ArrowUp') {
178 | e.preventDefault();
179 | state.selectedAutocompleteIndex = Math.max(state.selectedAutocompleteIndex - 1, -1);
180 | const selectedItem = currentVnode?.dom.querySelector('.autocomplete-item.selected') as HTMLElement;
181 | if (selectedItem) {
182 | selectedItem.scrollIntoView({ block: 'nearest' });
183 | }
184 | m.redraw();
185 | return;
186 | }
187 |
188 | if (e.key === 'Enter' && state.selectedAutocompleteIndex >= 0) {
189 | e.preventDefault();
190 | selectAutocompleteItem(state.autocompleteItems[state.selectedAutocompleteIndex]);
191 | return;
192 | }
193 | }
194 |
195 | if (e.key === 'Enter' && target.value.trim()) {
196 | e.preventDefault();
197 | addChip({ tag: target.value.trim() });
198 | } else if (e.key === 'Backspace' && !target.value && state.chipsData.length > 0) {
199 | e.preventDefault();
200 | // Delete the last chip immediately when backspace is pressed in an empty input
201 | deleteChip(state.chipsData.length - 1);
202 | } else if (e.key === 'ArrowLeft' && !target.value && state.chipsData.length) {
203 | e.preventDefault();
204 | selectChip(state.chipsData.length - 1);
205 | }
206 | };
207 |
208 | const handleChipKeydown = (e: KeyboardEvent, index: number) => {
209 | if (e.key === 'Backspace' || e.key === 'Delete') {
210 | e.preventDefault();
211 | deleteChip(index);
212 | const newIndex = Math.max(index - 1, 0);
213 | if (state.chipsData.length) selectChip(newIndex);
214 | } else if (e.key === 'ArrowLeft' && index > 0) {
215 | selectChip(index - 1);
216 | } else if (e.key === 'ArrowRight') {
217 | if (index < state.chipsData.length - 1) {
218 | selectChip(index + 1);
219 | } else {
220 | const input = currentVnode?.dom.querySelector('.chips-input') as HTMLInputElement;
221 | if (input) input.focus();
222 | }
223 | }
224 | };
225 |
226 | return {
227 | oninit: ({ attrs }) => {
228 | state.chipsData = attrs.data || [];
229 | },
230 |
231 | oncreate: (vnode) => {
232 | currentVnode = vnode;
233 | },
234 |
235 | onremove: () => {
236 | currentVnode = null;
237 | },
238 |
239 | view: ({ attrs }) => {
240 | const {
241 | id,
242 | required,
243 | isMandatory = required,
244 | className = 'col s12',
245 | label,
246 | helperText,
247 | placeholder,
248 | secondaryPlaceholder,
249 | } = attrs;
250 |
251 | const getPlaceholder = () => {
252 | if (!state.chipsData.length && placeholder) {
253 | return placeholder;
254 | }
255 | if (state.chipsData.length && secondaryPlaceholder) {
256 | return secondaryPlaceholder;
257 | }
258 | return '';
259 | };
260 |
261 | return m('.input-field', { id, className }, [
262 | m(
263 | '.chips.chips-initial',
264 | {
265 | class: `chips-container ${state.focused ? 'focused' : ''} ${placeholder ? 'chips-placeholder' : ''}`,
266 | },
267 | [
268 | // Chips
269 | state.chipsData.map((chip, index) =>
270 | m(
271 | '.chip',
272 | {
273 | key: `${chip.tag}-${index}`,
274 | tabindex: 0,
275 | class: state.selectedChip === index ? 'selected' : '',
276 | onkeydown: (e: KeyboardEvent) => handleChipKeydown(e, index),
277 | },
278 | [
279 | chip.image &&
280 | m('img', {
281 | src: chip.image,
282 | alt: chip.alt || chip.tag,
283 | }),
284 | chip.tag,
285 | m(
286 | 'i.material-icons.close',
287 | {
288 | onclick: (e: MouseEvent) => {
289 | e.stopPropagation();
290 | deleteChip(index);
291 | },
292 | },
293 | 'close'
294 | ),
295 | ]
296 | )
297 | ),
298 |
299 | // Input
300 | m('input.chips-input.input', {
301 | id: state.inputId,
302 | title: 'label',
303 | value: state.inputValue,
304 | placeholder: getPlaceholder(),
305 | oninput: (e: InputEvent) => {
306 | state.inputValue = (e.target as HTMLInputElement).value;
307 | updateAutocomplete();
308 | },
309 | onfocus: () => {
310 | state.focused = true;
311 | state.selectedChip = null;
312 | updateAutocomplete();
313 | },
314 | onblur: () => {
315 | state.focused = false;
316 | setTimeout(() => {
317 | state.showAutocomplete = false;
318 | state.selectedChip = null;
319 | m.redraw();
320 | }, 150);
321 | },
322 | onkeydown: handleKeydown,
323 | }),
324 |
325 | state.showAutocomplete &&
326 | m(
327 | 'ul.autocomplete-content.dropdown-content',
328 | {
329 | style: {
330 | display: 'block',
331 | opacity: 1,
332 | transform: 'scaleX(1) scaleY(1)',
333 | position: 'absolute',
334 | width: '100%',
335 | left: 0,
336 | top: '100%',
337 | maxHeight: '200px',
338 | overflow: 'auto',
339 | zIndex: 1000,
340 | backgroundColor: '#fff',
341 | boxShadow:
342 | '0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2)',
343 | },
344 | },
345 | state.autocompleteItems.map((item, index) =>
346 | m(
347 | 'li.autocomplete-item',
348 | {
349 | key: item.tag,
350 | class: state.selectedAutocompleteIndex === index ? 'selected' : '',
351 | style: {
352 | padding: '12px 16px',
353 | cursor: 'pointer',
354 | backgroundColor: state.selectedAutocompleteIndex === index ? '#eee' : 'transparent',
355 | },
356 | onmousedown: (e: MouseEvent) => {
357 | e.preventDefault();
358 | selectAutocompleteItem(item);
359 | },
360 | onmouseover: () => {
361 | state.selectedAutocompleteIndex = index;
362 | },
363 | },
364 | [
365 | item.image &&
366 | m('img.autocomplete-item-image', {
367 | src: item.image,
368 | alt: item.alt || item.tag,
369 | style: {
370 | width: '24px',
371 | height: '24px',
372 | marginRight: '8px',
373 | verticalAlign: 'middle',
374 | },
375 | }),
376 | m('span.autocomplete-item-text', item.tag),
377 | ]
378 | )
379 | )
380 | ),
381 | ]
382 | ),
383 | label &&
384 | m(Label, {
385 | label,
386 | id: state.inputId,
387 | isMandatory,
388 | isActive: state.focused || state.chipsData.length || placeholder ? true : false,
389 | }),
390 | helperText && m(HelperText, { helperText }),
391 | ]);
392 | },
393 | };
394 | };
395 |
--------------------------------------------------------------------------------
/packages/lib/src/code-block.ts:
--------------------------------------------------------------------------------
1 | import './styles/codeblock.css';
2 | import m, { FactoryComponent, Attributes } from 'mithril';
3 |
4 | export interface ICodeBlock extends Attributes {
5 | language?: string;
6 | code: string | string[];
7 | newRow?: boolean;
8 | }
9 |
10 | /** A simple code block without syntax high-lighting */
11 | export const CodeBlock: FactoryComponent = () => ({
12 | view: ({ attrs }) => {
13 | const { newRow, code, language, className, ...params } = attrs;
14 | const lang = language || 'lang-TypeScript';
15 | const label = lang.replace('lang-', '');
16 | const cb = code instanceof Array ? code.join('\n') : code;
17 | const cn = [newRow ? 'clear' : '', lang, className].filter(Boolean).join(' ').trim();
18 | return m(`pre.codeblock${newRow ? '.clear' : ''}`, attrs, [
19 | m('div', m('label', label)),
20 | m(
21 | 'code',
22 | {
23 | ...params,
24 | className: cn,
25 | },
26 | cb
27 | ),
28 | ]);
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/packages/lib/src/collapsible.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes, Vnode } from 'mithril';
2 |
3 | export interface ICollapsibleItem extends Attributes {
4 | /** Header of the collabsible item, may contain HTML or may be a Vnode */
5 | header?: string | Vnode;
6 | /** Body of the collabsible item, may contain HTML or may be a Vnode */
7 | body?: string | Vnode;
8 | /** If active, preselect the collabsible item. */
9 | active?: boolean;
10 | /** Add an material icon in front of the header. */
11 | iconName?: string;
12 | }
13 |
14 | export interface ICollapsible extends Partial, Attributes {
15 | /** The list of accordeon/collabsible items */
16 | items: ICollapsibleItem[];
17 | }
18 |
19 | export const CollapsibleItem: FactoryComponent = () => {
20 | return {
21 | view: ({ attrs: { header, body, active, iconName } }) => {
22 | return m(active ? 'li.active' : 'li', [
23 | header || iconName
24 | ? m('.collapsible-header', [
25 | iconName ? m('i.material-icons', iconName) : undefined,
26 | header ? (typeof header === 'string' ? m('span', header) : header) : undefined,
27 | ])
28 | : undefined,
29 | body ? m('.collapsible-body', typeof body === 'string' ? body : body) : undefined,
30 | ]);
31 | },
32 | };
33 | };
34 |
35 | /**
36 | * Creates a collabsible or accordion (via the accordion option, default true) component.
37 | * @see https://materializecss.com/collapsible.html
38 | */
39 | export const Collapsible: FactoryComponent = () => {
40 | return {
41 | oncreate: ({ dom, attrs }) => {
42 | M.Collapsible.init(dom, attrs);
43 | },
44 | view: ({ attrs }) => {
45 | const { items, class: c, className, style, id } = attrs;
46 | return items && items.length > 0
47 | ? m(
48 | 'ul.collapsible',
49 | {
50 | class: c || className,
51 | style,
52 | id,
53 | },
54 | items.map((item) => m(CollapsibleItem, item))
55 | )
56 | : undefined;
57 | },
58 | };
59 | };
60 |
--------------------------------------------------------------------------------
/packages/lib/src/collection.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes, Vnode } from 'mithril';
2 | import { Icon } from './icon';
3 |
4 | export enum CollectionMode {
5 | BASIC,
6 | LINKS,
7 | AVATAR,
8 | }
9 |
10 | export interface ICollectionItem {
11 | /** If available, will be used as the key, so all items need an id. */
12 | id?: string | number;
13 | /** Title of the collection item */
14 | title: string | Vnode;
15 | /** For links, may contain a URL reference */
16 | href?: string;
17 | /** For Avatar mode, may contain a URL reference to an image or a material icons class name */
18 | avatar?: string;
19 | /** Add a class to the avatar image or icon, e.g. a color 'red'. */
20 | className?: string;
21 | /** For Avatar mode, may contain a two-line trusted HTML content */
22 | content?: string;
23 | /** If active, preselect the collection item. */
24 | active?: boolean;
25 | /** Add a material icon as secondary content. */
26 | iconName?: string;
27 | /** Onclick event handler */
28 | onclick?: (item: ICollectionItem) => void;
29 | /** Any other virtual element properties, including attributes and event handlers. */
30 | [property: string]: any;
31 | }
32 |
33 | export interface ICollection extends Attributes {
34 | /** Optional header */
35 | header?: string;
36 | /** The list of items */
37 | items: ICollectionItem[];
38 | /** Mode of operation */
39 | mode?: CollectionMode;
40 | }
41 |
42 | const isNonLocalRoute = (url?: string) => url && /https?:\/\//.test(url);
43 |
44 | export const SecondaryContent: FactoryComponent = () => {
45 | return {
46 | view: ({ attrs }) => {
47 | const { href, iconName = 'send', onclick, style = { cursor: 'pointer' } } = attrs;
48 | const props = {
49 | href,
50 | style,
51 | className: 'secondary-content',
52 | onclick: onclick ? () => onclick(attrs) : undefined,
53 | };
54 | return isNonLocalRoute(href) || !href
55 | ? m('a[target=_]', props, m(Icon, { iconName }))
56 | : m(m.route.Link, props as { href: string }, m(Icon, { iconName }));
57 | },
58 | };
59 | };
60 |
61 | const avatarIsImage = (avatar = '') => /\./.test(avatar);
62 |
63 | export const ListItem: FactoryComponent<{ item: ICollectionItem; mode: CollectionMode }> = () => {
64 | return {
65 | view: ({ attrs: { item, mode } }) => {
66 | const { title, content = '', active, iconName, avatar, className, onclick } = item;
67 | return mode === CollectionMode.AVATAR
68 | ? m(
69 | 'li.collection-item.avatar',
70 | {
71 | className: active ? 'active' : '',
72 | onclick: onclick ? () => onclick(item) : undefined,
73 | },
74 | [
75 | avatarIsImage(avatar)
76 | ? m('img.circle', { src: avatar })
77 | : m('i.material-icons.circle', { className }, avatar),
78 | m('span.title', title),
79 | m('p', m.trust(content)),
80 | m(SecondaryContent, item),
81 | ]
82 | )
83 | : m(
84 | 'li.collection-item',
85 | {
86 | className: active ? 'active' : '',
87 | },
88 | iconName ? m('div', [title, m(SecondaryContent, item)]) : title
89 | );
90 | },
91 | };
92 | };
93 |
94 | const BasicCollection: FactoryComponent = () => {
95 | return {
96 | view: ({ attrs: { header, items, mode = CollectionMode.BASIC, ...params } }) => {
97 | const collectionItems = items.map((item) => m(ListItem, { key: item.id, item, mode }));
98 | return header
99 | ? m('ul.collection.with-header', params, [m('li.collection-header', m('h4', header)), collectionItems])
100 | : m('ul.collection', params, collectionItems);
101 | },
102 | };
103 | };
104 |
105 | export const AnchorItem: FactoryComponent<{ item: ICollectionItem }> = () => {
106 | return {
107 | view: ({ attrs: { item } }) => {
108 | const { title, active, href, ...params } = item;
109 | const props = {
110 | ...params,
111 | className: `collection-item ${active ? 'active' : ''}`,
112 | href,
113 | };
114 | return isNonLocalRoute(href) || !href
115 | ? m('a[target=_]', props, title)
116 | : m(m.route.Link, props as { href: string }, title);
117 | },
118 | };
119 | };
120 |
121 | const LinksCollection: FactoryComponent = () => {
122 | return {
123 | view: ({ attrs: { items, header, ...params } }) => {
124 | return header
125 | ? m('.collection.with-header', params, [
126 | m('.collection-header', m('h4', header)),
127 | items.map((item) => m(AnchorItem, { key: item.id, item })),
128 | ])
129 | : m(
130 | '.collection',
131 | params,
132 | items.map((item) => m(AnchorItem, { key: item.id, item }))
133 | );
134 | },
135 | };
136 | };
137 |
138 | /**
139 | * Creates a Collection of items, optionally containing links, headers, secondary content or avatars.
140 | * @see https://materializecss.com/collections.html
141 | */
142 | export const Collection: FactoryComponent = () => {
143 | return {
144 | view: ({ attrs: { items, header, mode = CollectionMode.BASIC, ...params } }) => {
145 | return header || (items && items.length > 0)
146 | ? mode === CollectionMode.LINKS
147 | ? m(LinksCollection, { header, items, ...params })
148 | : m(BasicCollection, { header, items, mode, ...params })
149 | : undefined;
150 | },
151 | };
152 | };
153 |
--------------------------------------------------------------------------------
/packages/lib/src/dropdown.ts:
--------------------------------------------------------------------------------
1 | import m, { Component, Attributes } from 'mithril';
2 | import { HelperText } from './label';
3 | import { uniqueId } from './utils';
4 |
5 | export interface IDropdownOption {
6 | /** ID property of the selected item */
7 | id?: T;
8 | /** Label to show in the dropdown */
9 | label: string;
10 | /** Can we select the item */
11 | disabled?: boolean;
12 | /** Display a Materials Icon in front of the label */
13 | iconName?: string;
14 | /** Add a divider */
15 | divider?: boolean;
16 | }
17 |
18 | export interface IDropdownOptions extends Partial, Attributes {
19 | /**
20 | * Optional id of the dropdown element
21 | * @default 'dropdown'
22 | */
23 | id?: T;
24 | /**
25 | * Optional label when no item is selected
26 | * @default 'Select'
27 | */
28 | label?: string;
29 | key?: string | number;
30 | /** If true, disable the selection */
31 | disabled?: boolean;
32 | /** Item array to show in the dropdown. If the value is not supplied, uses he name. */
33 | items: IDropdownOption[];
34 | /**
35 | * Selected value or name
36 | * @deprecated Use initialValue instead
37 | */
38 | checkedId?: T;
39 | /** Selected value or name */
40 | initialValue?: T;
41 | /** When a value or name is selected */
42 | onchange?: (value: T) => void;
43 | /** Uses Materialize icons as a prefix or postfix. */
44 | iconName?: string;
45 | /** Add a description underneath the input field. */
46 | helperText?: string;
47 | }
48 |
49 | /** Dropdown component */
50 | export const Dropdown = (): Component> => {
51 | // export const Dropdown: FactoryComponent = () => {
52 | const state = {} as {
53 | initialValue?: T;
54 | id: T;
55 | };
56 | return {
57 | oninit: ({ attrs: { id = uniqueId(), initialValue, checkedId } }) => {
58 | state.id = id as T;
59 | state.initialValue = initialValue || checkedId;
60 | },
61 | view: ({
62 | attrs: {
63 | key,
64 | label,
65 | onchange,
66 | disabled = false,
67 | items,
68 | iconName,
69 | helperText,
70 | style,
71 | className = 'col s12',
72 | ...props
73 | },
74 | }) => {
75 | const { id, initialValue } = state;
76 | const selectedItem = initialValue
77 | ? items.filter((i: IDropdownOption) => (i.id ? i.id === initialValue : i.label === initialValue)).shift()
78 | : undefined;
79 | const title = selectedItem ? selectedItem.label : label || 'Select';
80 | return m('.input-field', { className, key, style }, [
81 | iconName ? m('i.material-icons.prefix', iconName) : undefined,
82 | m(HelperText, { helperText }),
83 | m(
84 | 'a.dropdown-trigger.btn.truncate[href=#]',
85 | {
86 | 'data-target': id,
87 | disabled,
88 | className: 'col s12',
89 | style: style || (iconName ? 'margin: 0.2em 0 0 3em;' : undefined),
90 | oncreate: ({ dom }) => {
91 | M.Dropdown.init(dom, props);
92 | },
93 | },
94 | title
95 | ),
96 | m(
97 | 'ul.dropdown-content',
98 | { id },
99 | items.map((i) =>
100 | m(
101 | 'li[tabindex=-1]',
102 | {
103 | className: i.divider ? 'divider' : '',
104 | },
105 | i.divider
106 | ? undefined
107 | : m(
108 | 'a',
109 | {
110 | onclick: onchange
111 | ? () => {
112 | state.initialValue = (i.id || i.label) as T;
113 | onchange(state.initialValue);
114 | }
115 | : undefined,
116 | },
117 | [i.iconName ? m('i.material-icons', i.iconName) : undefined, i.label]
118 | )
119 | )
120 | )
121 | ),
122 | ]);
123 | },
124 | };
125 | };
126 |
--------------------------------------------------------------------------------
/packages/lib/src/floating-action-button.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent } from 'mithril';
2 |
3 | export interface IFloatingActionButton extends Partial {
4 | /** Optional classes to add to the top element */
5 | className?: string;
6 | /** Optional style to add to the top element, e.g. for positioning it inline */
7 | style?: string;
8 | /** Material-icons name for the main FAB, @see https://materializecss.com/icons.html */
9 | iconName: string;
10 | /** Helper option to place the FAB inline instead of the bottom right of the display */
11 | position?: 'left' | 'right' | 'inline-left' | 'inline-right';
12 | /**
13 | * Optional icon class, e.g. tiny (1em), small (2em), medium (4em), large (6em), or 'tiny right'.
14 | * @default large
15 | */
16 | iconClass?: string;
17 | /** The buttons you want to show */
18 | buttons?: Array<{
19 | /** Optional classes you want to add to the main element */
20 | className?: string;
21 | /** Name of the icon */
22 | iconName: string;
23 | /** Classes of the icon */
24 | iconClass?: string;
25 | /** On click function */
26 | onClick?: (e: UIEvent) => void;
27 | }>;
28 | }
29 |
30 | /**
31 | * A Floating Action Button.
32 | *
33 | * @example FlatButton = ButtonFactory('a.waves-effect.waves-teal.btn-flat');
34 | */
35 | export const FloatingActionButton: FactoryComponent = () => {
36 | return {
37 | view: ({
38 | attrs: {
39 | className,
40 | iconName,
41 | iconClass = 'large',
42 | position,
43 | style = position === 'left' || position === 'inline-left'
44 | ? 'position: absolute; display: inline-block; left: 24px;'
45 | : position === 'right' || position === 'inline-right'
46 | ? 'position: absolute; display: inline-block; right: 24px;'
47 | : undefined,
48 | buttons,
49 | ...options
50 | },
51 | }) => {
52 | const fab = m(
53 | '.fixed-action-btn',
54 | {
55 | style,
56 | oncreate: ({ dom }) => M.FloatingActionButton.init(dom, options),
57 | },
58 | [
59 | m('a.btn-floating.btn-large', { className }, m('i.material-icons', { classNames: iconClass }, iconName)),
60 | buttons
61 | ? m(
62 | 'ul',
63 | buttons.map(b =>
64 | m(
65 | 'li',
66 | m(
67 | 'a.btn-floating',
68 | { className: b.className, onclick: (e: UIEvent) => b.onClick && b.onClick(e) },
69 | m('i.material-icons', { className: b.iconClass }, b.iconName)
70 | )
71 | )
72 | )
73 | )
74 | : undefined,
75 | ]
76 | );
77 | return position === 'inline-right' || position === 'inline-left'
78 | ? m('div', { style: 'position: relative; height: 70px;' }, fab)
79 | : fab;
80 | },
81 | };
82 | };
83 |
--------------------------------------------------------------------------------
/packages/lib/src/icon.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes } from 'mithril';
2 |
3 | export interface IMaterialIcon extends Attributes {
4 | iconName: string;
5 | }
6 |
7 | /**
8 | * A simple material icon, defined by its icon name.
9 | *
10 | * @example m(Icon, { className: 'small' }, 'create') renders a small 'create' icon
11 | * @example m(Icon, { className: 'prefix' }, iconName) renders the icon as a prefix
12 | */
13 | export const Icon: FactoryComponent = () => ({
14 | view: ({ attrs: { iconName, ...passThrough } }) => m('i.material-icons', passThrough, iconName),
15 | });
16 |
--------------------------------------------------------------------------------
/packages/lib/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './autocomplete';
2 | export * from './button';
3 | export * from './carousel';
4 | export * from './chip';
5 | export * from './code-block';
6 | export * from './collapsible';
7 | export * from './collection';
8 | export * from './dropdown';
9 | export * from './floating-action-button';
10 | export * from './icon';
11 | export * from './input-options';
12 | export * from './input';
13 | export * from './label';
14 | export * from './map-editor';
15 | export * from './material-box';
16 | export * from './modal';
17 | export * from './option';
18 | export * from './pagination';
19 | export * from './parallax';
20 | export * from './pickers';
21 | export * from './radio';
22 | export * from './select';
23 | export * from './switch';
24 | export * from './tabs';
25 | export * from './timeline';
26 | export * from './search-select';
27 | export * from './utils';
28 |
--------------------------------------------------------------------------------
/packages/lib/src/input-options.ts:
--------------------------------------------------------------------------------
1 | import { Attributes } from 'mithril';
2 |
3 | export interface IInputOptions extends Attributes {
4 | /** Optional label. */
5 | label?: string;
6 | /** Optional ID. */
7 | id?: string;
8 | /** Unique key for use of the element in an array. */
9 | key?: string | number;
10 | /** Initial value of the input field. */
11 | initialValue?: T;
12 | /**
13 | * The autocomplete property sets or returns the value of the autocomplete
14 | * attribute in a text field. When autocomplete is on, the browser automatically
15 | * complete values based on values that the user has entered before.
16 | * @default 'on'
17 | */
18 | autocomplete?: 'on' | 'off';
19 | /**
20 | * The pattern property sets or returns the value of the pattern attribute of
21 | * a text field. The pattern attribute specifies a regular expression that the
22 | * text field's value is checked against.
23 | */
24 | pattern?: RegExp;
25 | /**
26 | * The readOnly property sets or returns whether a text field is read-only, or not.
27 | * A read-only field cannot be modified. However, a user can tab to it, highlight it,
28 | * and copy the text from it.
29 | */
30 | readOnly?: boolean;
31 | /** When true, add the autofocus attribute to the input field. */
32 | autofocus?: (() => boolean) | boolean;
33 | /** Key down event */
34 | onkeydown?: (ev: KeyboardEvent, value?: T) => void;
35 | /** Key press event */
36 | onkeypress?: (ev: KeyboardEvent, value?: T) => void;
37 | /** Key up event */
38 | onkeyup?: (ev: KeyboardEvent, value?: T) => void;
39 | /** Invoked when the element looses focus */
40 | onblur?: (ev: FocusEvent) => void;
41 | /** Invoked when the value changes. */
42 | onchange?: (value: T) => void;
43 | /** Add a a placeholder to the input field. */
44 | placeholder?: string;
45 | /** Add a description underneath the input field. */
46 | helperText?: string;
47 | /**
48 | * When returning true or an empty string, clear the custom validity (= valid).
49 | * When returning false, set the custom validity message to a default string string.
50 | * When returning a non-empty string, set the custom validity message to this string.
51 | */
52 | validate?: (v: T, target?: HTMLInputElement) => boolean | string;
53 | /** Will replace the helperText, if any, when the input is invalid. */
54 | dataError?: string;
55 | /** Will replace the helperText, if any, when the input is valid. */
56 | dataSuccess?: string;
57 | /** Uses Materialize icons as a prefix or postfix. */
58 | iconName?: string;
59 | /** Sets the input field to disabled. */
60 | disabled?: boolean;
61 | /** Optional style information. */
62 | style?: string;
63 | /** When input type is a number, optionally specify the minimum value. */
64 | min?: number;
65 | /** When input type is a number, optionally specify the maximum value. */
66 | max?: number;
67 | /** When input type is a text or text area, optionally specify the minimum length. */
68 | minLength?: number;
69 | /** When input type is a text or text area, optionally specify the maximum length. */
70 | maxLength?: number;
71 | /** Number of rows of a textarea */
72 | rows?: number;
73 | /** Number of cols of a textarea */
74 | cols?: number;
75 | /** If true, break to a new row */
76 | newRow?: boolean;
77 | /**
78 | * If true, add a mandatory * after the label (if any),
79 | * and add the required and aria-required attributes to the input element.
80 | */
81 | isMandatory?: boolean;
82 | /** Add the required and aria-required attributes to the input element */
83 | required?: boolean;
84 | }
85 |
--------------------------------------------------------------------------------
/packages/lib/src/input.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes } from 'mithril';
2 | import { uniqueId } from './utils';
3 | import { IInputOptions } from './input-options';
4 | import { Label, HelperText } from './label';
5 | import './styles/input.css';
6 |
7 | /** Create a TextArea */
8 | export const TextArea: FactoryComponent> = () => {
9 | const state = { id: uniqueId() };
10 | return {
11 | view: ({ attrs }) => {
12 | const {
13 | className = 'col s12',
14 | helperText,
15 | iconName,
16 | id = state.id,
17 | initialValue,
18 | isMandatory,
19 | label,
20 | onchange,
21 | onkeydown,
22 | onkeypress,
23 | onkeyup,
24 | onblur,
25 | style,
26 | ...params
27 | } = attrs;
28 | // const attributes = toAttrs(params);
29 | return m('.input-field', { className, style }, [
30 | iconName ? m('i.material-icons.prefix', iconName) : '',
31 | m('textarea.materialize-textarea', {
32 | ...params,
33 | id,
34 | tabindex: 0,
35 | oncreate: ({ dom }) => {
36 | M.textareaAutoResize(dom);
37 | if (attrs.maxLength) {
38 | M.CharacterCounter.init(dom);
39 | }
40 | },
41 | onchange: onchange
42 | ? (e: Event) => {
43 | const target = e.target as HTMLInputElement;
44 | const value = target && typeof target.value === 'string' ? target.value : '';
45 | onchange(value);
46 | }
47 | : undefined,
48 | value: initialValue,
49 | onkeyup: onkeyup
50 | ? (ev: KeyboardEvent) => {
51 | onkeyup(ev, (ev.target as HTMLTextAreaElement).value);
52 | }
53 | : undefined,
54 | onkeydown: onkeydown
55 | ? (ev: KeyboardEvent) => {
56 | onkeydown(ev, (ev.target as HTMLTextAreaElement).value);
57 | }
58 | : undefined,
59 | onkeypress: onkeypress
60 | ? (ev: KeyboardEvent) => {
61 | onkeypress(ev, (ev.target as HTMLTextAreaElement).value);
62 | }
63 | : undefined,
64 | onblur,
65 | }),
66 | m(Label, { label, id, isMandatory, isActive: initialValue || attrs.placeholder }),
67 | m(HelperText, { helperText }),
68 | ]);
69 | },
70 | };
71 | };
72 |
73 | export type InputType = 'url' | 'color' | 'text' | 'number' | 'email' | 'range' | 'password';
74 |
75 | /** Default component for all kinds of input fields. */
76 | const InputField =
77 | (type: InputType, defaultClass = ''): FactoryComponent> =>
78 | () => {
79 | const state = { id: uniqueId() };
80 | const getValue = (target: HTMLInputElement) => {
81 | const val = target.value as any as T;
82 | return (val ? (type === 'number' || type === 'range' ? +val : val) : val) as T;
83 | };
84 | const setValidity = (target: HTMLInputElement, validationResult: string | boolean) => {
85 | if (typeof validationResult === 'boolean') {
86 | target.setCustomValidity(validationResult ? '' : 'Custom validation failed');
87 | } else {
88 | target.setCustomValidity(validationResult);
89 | }
90 | };
91 | const focus = ({ autofocus }: IInputOptions) =>
92 | autofocus ? (typeof autofocus === 'boolean' ? autofocus : autofocus()) : false;
93 |
94 | return {
95 | view: ({ attrs }) => {
96 | const {
97 | className = 'col s12',
98 | dataError,
99 | dataSuccess,
100 | helperText,
101 | iconName,
102 | id = state.id,
103 | initialValue,
104 | isMandatory,
105 | label,
106 | maxLength,
107 | newRow,
108 | onchange,
109 | onkeydown,
110 | onkeypress,
111 | onkeyup,
112 | onblur,
113 | style,
114 | validate,
115 | ...params
116 | } = attrs;
117 | // const attributes = toAttrs(params);
118 | const cn = [newRow ? 'clear' : '', defaultClass, className].filter(Boolean).join(' ').trim();
119 | return m('.input-field', { className: cn, style }, [
120 | iconName ? m('i.material-icons.prefix', iconName) : undefined,
121 | m('input.validate', {
122 | ...params,
123 | type,
124 | tabindex: 0,
125 | id,
126 | // attributes,
127 | oncreate: ({ dom }) => {
128 | if (focus(attrs)) {
129 | (dom as HTMLElement).focus();
130 | }
131 | if (maxLength) {
132 | M.CharacterCounter.init(dom);
133 | }
134 | if (type === 'range') {
135 | M.Range.init(dom);
136 | }
137 | },
138 | onkeyup: onkeyup
139 | ? (ev: KeyboardEvent) => {
140 | onkeyup(ev, getValue(ev.target as HTMLInputElement));
141 | }
142 | : undefined,
143 | onkeydown: onkeydown
144 | ? (ev: KeyboardEvent) => {
145 | onkeydown(ev, getValue(ev.target as HTMLInputElement));
146 | }
147 | : undefined,
148 | onkeypress: onkeypress
149 | ? (ev: KeyboardEvent) => {
150 | onkeypress(ev, getValue(ev.target as HTMLInputElement));
151 | }
152 | : undefined,
153 | onblur,
154 | onupdate: validate
155 | ? ({ dom }) => {
156 | const target = dom as HTMLInputElement;
157 | setValidity(target, validate(getValue(target), target));
158 | }
159 | : undefined,
160 | onchange: (e: UIEvent) => {
161 | const target = e.target as HTMLInputElement;
162 | if (target) {
163 | const value = getValue(target);
164 | if (onchange) {
165 | onchange(value);
166 | }
167 | if (validate) {
168 | setValidity(target, validate(value, target));
169 | }
170 | }
171 | },
172 | value: initialValue,
173 | }),
174 | m(Label, {
175 | label,
176 | id,
177 | isMandatory,
178 | isActive:
179 | typeof initialValue !== 'undefined' ||
180 | attrs.placeholder ||
181 | type === 'number' ||
182 | type === 'color' ||
183 | type === 'range'
184 | ? true
185 | : false,
186 | }),
187 | m(HelperText, { helperText, dataError, dataSuccess }),
188 | ]);
189 | },
190 | };
191 | };
192 |
193 | /** Component for entering some text */
194 | export const TextInput = InputField('text');
195 | /** Component for entering a password */
196 | export const PasswordInput = InputField('password');
197 | /** Component for entering a number */
198 | export const NumberInput = InputField('number');
199 | /** Component for entering a URL */
200 | export const UrlInput = InputField('url');
201 | /** Component for entering a color */
202 | export const ColorInput = InputField('color');
203 | /** Component for entering a range */
204 | export const RangeInput = InputField('range', '.range-field');
205 | /** Component for entering an email */
206 | export const EmailInput = InputField('email');
207 |
208 | export interface IFileInputOptions extends Attributes {
209 | /** Displayed on the button, @default File */
210 | label?: string;
211 | /** Current value of the file input, write only */
212 | initialValue?: string;
213 | /** Adds a placeholder message */
214 | placeholder?: string;
215 | /** If true, upload multiple files */
216 | multiple?: boolean;
217 | /** Called when the file input is changed */
218 | onchange?: (files: FileList) => void;
219 | /** If true, disable the box */
220 | disabled?: boolean;
221 | /**
222 | * Accepted file types, e.g. image/png, image/jpeg,
223 | * any image/*, video/*. audio/*, .pdf, a valid MIME type string, with no extensions, etc.
224 | * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers
225 | */
226 | accept?: string | string[];
227 | }
228 |
229 | /** Component for uploading a file */
230 | export const FileInput: FactoryComponent = () => {
231 | let canClear = false;
232 | let i: HTMLInputElement;
233 | return {
234 | view: ({ attrs }) => {
235 | const {
236 | multiple,
237 | disabled,
238 | initialValue,
239 | placeholder,
240 | onchange,
241 | className = 'col s12',
242 | accept: acceptedFiles,
243 | label = 'File',
244 | } = attrs;
245 | const accept = acceptedFiles
246 | ? acceptedFiles instanceof Array
247 | ? acceptedFiles.join(', ')
248 | : acceptedFiles
249 | : undefined;
250 | return m(
251 | '.file-field.input-field',
252 | {
253 | className: attrs.class || className,
254 | },
255 | [
256 | m('.btn', [
257 | m('span', label),
258 | m('input[type=file]', {
259 | title: label,
260 | accept,
261 | multiple,
262 | disabled,
263 | onchange: onchange
264 | ? (e: UIEvent) => {
265 | const i = e.target as HTMLInputElement;
266 | if (i && i.files && onchange) {
267 | canClear = true;
268 | onchange(i.files);
269 | }
270 | }
271 | : undefined,
272 | }),
273 | ]),
274 | m(
275 | '.file-path-wrapper',
276 | m('input.file-path.validate[type=text]', {
277 | placeholder,
278 | oncreate: ({ dom }) => {
279 | i = dom as HTMLInputElement;
280 | if (initialValue) i.value = initialValue;
281 | },
282 | })
283 | ),
284 | (canClear || initialValue) &&
285 | m(
286 | 'a.waves-effect.waves-teal.btn-flat',
287 | {
288 | style: {
289 | float: 'right',
290 | position: 'relative',
291 | top: '-3rem',
292 | padding: 0,
293 | },
294 | onclick: () => {
295 | canClear = false;
296 | i.value = '';
297 | onchange && onchange({} as FileList);
298 | },
299 | },
300 | m('i.material-icons', 'clear')
301 | ),
302 | ]
303 | );
304 | },
305 | };
306 | };
307 |
--------------------------------------------------------------------------------
/packages/lib/src/label.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Component, Attributes } from 'mithril';
2 | import './styles/input.css';
3 |
4 | export const Mandatory: Component = { view: ({ attrs }) => m('span.mandatory', attrs, '*') };
5 |
6 | export interface IMaterialLabel extends Attributes {
7 | /** Optional title/label */
8 | label?: string;
9 | /** Optional ID */
10 | id?: string;
11 | /** If true, add a mandatory '*' after the label */
12 | isMandatory?: boolean;
13 | /** Add the active class to the label */
14 | isActive?: boolean | string;
15 | }
16 |
17 | /** Simple label element, used for most components. */
18 | export const Label: FactoryComponent = () => {
19 | return {
20 | view: ({ attrs: { label, id, isMandatory, isActive, className, ...params } }) =>
21 | label
22 | ? m(
23 | 'label',
24 | {
25 | ...params,
26 | className: [className, isActive ? 'active' : ''].filter(Boolean).join(' ').trim(),
27 | for: id,
28 | },
29 | [m.trust(label), isMandatory ? m(Mandatory) : undefined]
30 | )
31 | : undefined,
32 | };
33 | };
34 |
35 | export interface IHelperTextOptions extends Attributes {
36 | helperText?: string;
37 | dataError?: string;
38 | dataSuccess?: string;
39 | }
40 |
41 | /** Create a helper text, often used for displaying a small help text. May be replaced by the validation message. */
42 | export const HelperText: FactoryComponent = () => {
43 | return {
44 | view: ({ attrs: { helperText, dataError, dataSuccess, className } }) => {
45 | return helperText || dataError || dataSuccess
46 | ? m(
47 | 'span.helper-text.left',
48 | { className, 'data-error': dataError, 'data-success': dataSuccess },
49 | helperText ? m.trust(helperText) : ''
50 | )
51 | : undefined;
52 | },
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/packages/lib/src/map-editor.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes } from 'mithril';
2 | import './styles/map-editor.css';
3 | import { ICollectionItem, Collection, CollectionMode } from './collection';
4 | import { InputCheckbox } from './option';
5 | import { uniqueId } from './utils';
6 | import { Label } from './label';
7 | import { TextArea, TextInput, NumberInput } from './input';
8 | import { FlatButton } from './button';
9 |
10 | export interface IMapEditor extends Attributes {
11 | /** Optional ID of the element */
12 | id?: string;
13 | /** If true, displays a header over the map */
14 | header?: string;
15 | /** Instead of a header, use a label */
16 | label?: string;
17 | /** Places a required * after the label */
18 | isMandatory?: boolean;
19 | /**
20 | * Optional value for the key label
21 | * @default: "Key"
22 | */
23 | labelKey?: string;
24 | /**
25 | * Optional value for the value label
26 | * @default: "Value"
27 | */
28 | labelValue?: string;
29 | /** If true, the item cannot be edited */
30 | disabled?: boolean;
31 | /** Icon for the properties' collection */
32 | iconName?: string;
33 | /** Icon for the key editor: if none provided, and the iconName is set, uses 'label' */
34 | iconNameKey?: string;
35 | /** If true, do not parse arrays like [1, 2, 3] into number[] or [a, b, c] into a string[] */
36 | disallowArrays?: boolean;
37 | /** The actual map of key-value pairs supports numbers, strings, booleans and arrays of strings and numbers. */
38 | properties: { [key: string]: number | string | boolean | Array };
39 | /**
40 | * Called when the properties collection has changed. Not needed if you are performing a direct edit on the
41 | * properties object, but in case you have created a mapping, this allows you to convert the object back again.
42 | */
43 | onchange?: (properties: { [key: string]: number | string | boolean | Array }) => void;
44 | /**
45 | * In order to create a boolean, you first have to enter a truthy or falsy value.
46 | * Default 'true' and 'false', but you can add more options.
47 | */
48 | truthy?: string[];
49 | /**
50 | * In order to create a boolean, you first have to enter a truthy or falsy value.
51 | * Default 'true' and 'false', but you can add more options.
52 | */
53 | falsy?: string[];
54 | /**
55 | * Optional function to replace the render function of a key-value pair.
56 | * The ICollectionItems's title may be a Vnode.
57 | */
58 | keyValueConverter?: (key: string, value: number | string | boolean | Array) => ICollectionItem;
59 | /** Optional class to apply to the key column, @default .col.s4 */
60 | keyClass?: string;
61 | /** Optional class to apply to the value column, @default .col.s8 */
62 | valueClass?: string;
63 | }
64 |
65 | /** A simple viewer and/or editor for a map of key - value pairs */
66 | export const MapEditor: FactoryComponent = () => {
67 | const parseArray = (v?: string, disallowArrays = false) => {
68 | if (disallowArrays) {
69 | return v;
70 | }
71 | const extractArrayData = /\s*\[(.*)\]\s*/gi;
72 | if (!v) {
73 | return undefined;
74 | }
75 | const match = extractArrayData.exec(v);
76 | if (!match || match.length !== 2) {
77 | return undefined;
78 | }
79 | return match[1]
80 | .split(',')
81 | .map(i => i.trim())
82 | .map(i => (/^\d+$/g.test(i) ? +i : i));
83 | };
84 |
85 | const kvc = (
86 | key: string,
87 | value: number | string | boolean | Array,
88 | options: { keyClass?: string; valueClass?: string }
89 | ) => {
90 | const { keyClass = '.col.s4', valueClass = '.col.s8' } = options;
91 | const displayValue =
92 | value instanceof Array
93 | ? value.join(', ')
94 | : typeof value === 'boolean'
95 | ? m(InputCheckbox, { label: ' ', checked: value, disabled: true, className: 'checkbox-in-collection' })
96 | : value.toString();
97 | const title = m('.row', { style: 'margin-bottom: 0' }, [m(keyClass, m('b', key)), m(valueClass, displayValue)]);
98 | return {
99 | title,
100 | } as ICollectionItem;
101 | };
102 |
103 | const onclick = (key: string) => (state.curKey = state.id = key);
104 |
105 | const kvcWrapper = (key: string, item: ICollectionItem) => {
106 | const clickHandler = item.onclick;
107 | item.id = item.id || key;
108 | item.active = key === state.curKey;
109 | item.onclick = clickHandler ? () => onclick(key) && clickHandler(item) : () => onclick(key);
110 | return item;
111 | };
112 |
113 | const toCollectionArray = (
114 | properties: { [key: string]: number | string | boolean | Array },
115 | options: { keyClass?: string; valueClass?: string }
116 | ) =>
117 | Object.keys(properties)
118 | .map(key => ({ key, value: properties[key] }))
119 | .map(item =>
120 | kvcWrapper(
121 | item.key,
122 | state.kvc(item.key, item.value, { keyClass: options.keyClass, valueClass: options.valueClass })
123 | )
124 | );
125 |
126 | const isTruthy = (i: string, truthy: string[], falsy: string[]) =>
127 | truthy.indexOf(i) >= 0 ? true : falsy.indexOf(i) >= 0 ? false : undefined;
128 |
129 | const state = {
130 | elementId: uniqueId(),
131 | id: '',
132 | curKey: '',
133 | kvc,
134 | };
135 |
136 | const resetInputs = () => {
137 | state.id = '';
138 | state.curKey = '';
139 | };
140 |
141 | return {
142 | oninit: ({ attrs: { keyValueConverter, id } }) => {
143 | if (keyValueConverter) {
144 | state.kvc = keyValueConverter;
145 | }
146 | if (id) {
147 | state.elementId = id;
148 | }
149 | },
150 | view: ({
151 | attrs: {
152 | className = 'col s12',
153 | disabled,
154 | disallowArrays,
155 | header,
156 | iconName,
157 | iconNameKey = iconName ? 'label' : undefined,
158 | isMandatory,
159 | label,
160 | labelKey = 'Key',
161 | labelValue = 'Value',
162 | properties,
163 | keyClass,
164 | valueClass,
165 | onchange,
166 | falsy = ['false'],
167 | truthy = ['true'],
168 | },
169 | }) => {
170 | const notify = () => (onchange ? onchange(properties) : undefined);
171 | const items = toCollectionArray(properties, { keyClass, valueClass });
172 | const key = state.curKey;
173 | const prop = properties[key];
174 | const value =
175 | typeof prop === 'boolean' || typeof prop === 'number'
176 | ? prop
177 | : prop
178 | ? prop instanceof Array
179 | ? `[${prop.join(', ')}]`
180 | : prop
181 | : '';
182 | const id = state.elementId;
183 | return [
184 | m(
185 | '.map-editor',
186 | m('.input-field', { className, style: 'min-height: 1.5em;' }, [
187 | iconName ? m('i.material-icons.prefix', iconName) : '',
188 | m(Label, { label, isMandatory, isActive: items.length > 0 }),
189 | m(Collection, { id, items, mode: CollectionMode.LINKS, header }),
190 | ])
191 | ),
192 | disabled
193 | ? undefined
194 | : [
195 | m(TextInput, {
196 | label: labelKey,
197 | iconName: iconNameKey,
198 | className: 'col s5',
199 | initialValue: key,
200 | onchange: (v: string) => {
201 | state.curKey = v;
202 | if (state.id) {
203 | delete properties[state.id];
204 | properties[v] = prop;
205 | state.id = v;
206 | }
207 | notify();
208 | },
209 | }),
210 | typeof value === 'string'
211 | ? m(TextArea, {
212 | label: labelValue,
213 | initialValue: value,
214 | className: 'col s7',
215 | onchange: (v: string) => {
216 | const b = isTruthy(v, truthy, falsy);
217 | const n = typeof b === 'undefined' ? (/^\s*\d+\s*$/i.test(v) ? +v : undefined) : undefined;
218 | properties[key] =
219 | typeof b === 'boolean' ? b : typeof n === 'number' ? n : parseArray(v, disallowArrays) || v;
220 | notify();
221 | },
222 | })
223 | : typeof value === 'number'
224 | ? m(NumberInput, {
225 | label: labelValue,
226 | initialValue: value,
227 | className: 'col s7',
228 | onchange: (v: number) => {
229 | properties[key] = v;
230 | notify();
231 | },
232 | })
233 | : m(InputCheckbox, {
234 | label: labelValue,
235 | checked: value,
236 | className: 'input-field col s7',
237 | onchange: (v: boolean) => {
238 | properties[key] = v;
239 | notify();
240 | },
241 | }),
242 | m('.col.s12.right-align', [
243 | m(FlatButton, {
244 | iconName: 'add',
245 | onclick: resetInputs,
246 | }),
247 | m(FlatButton, {
248 | iconName: 'delete',
249 | disabled: !key,
250 | onclick: () => {
251 | delete properties[key];
252 | resetInputs();
253 | notify();
254 | },
255 | }),
256 | ]),
257 | ],
258 | ];
259 | },
260 | };
261 | };
262 |
--------------------------------------------------------------------------------
/packages/lib/src/material-box.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes } from 'mithril';
2 |
3 | export interface IMaterialBox extends Partial, Attributes {
4 | /** Source image path */
5 | src: string;
6 | /**
7 | * Width of the image
8 | * @default undefined
9 | */
10 | width?: number;
11 | /**
12 | * Height of the image
13 | * @default undefined
14 | */
15 | height?: number;
16 | }
17 |
18 | /**
19 | * Create an image box, that, when clicked upon, fills the screen.
20 | * @see https://materializecss.com/media.html
21 | */
22 | export const MaterialBox: FactoryComponent = () => {
23 | return {
24 | oncreate: ({ dom, attrs }) => {
25 | M.Materialbox.init(dom, attrs);
26 | },
27 | view: ({ attrs }) => m('img.materialboxed', attrs),
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/packages/lib/src/modal.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Vnode, Attributes } from 'mithril';
2 | import { FlatButton } from './button';
3 |
4 | export interface IMaterialModal extends Attributes {
5 | id: string;
6 | title: string;
7 | description?: string | Vnode;
8 | /** Set to true when the description contains HTML */
9 | richContent?: boolean;
10 | /** Fixate the footer, so you can show more content. */
11 | fixedFooter?: boolean;
12 | /** Display on the bottom */
13 | bottomSheet?: boolean;
14 | /** Materialize css' modal options */
15 | options?: Partial;
16 | /** Menu buttons, from left to right */
17 | buttons?: Array<{ label: string; iconName?: string; disabled?: boolean; onclick?: (e: UIEvent) => void }>;
18 | /** Get the modal instance, so you can control it programmatically */
19 | onCreate?: (modal: M.Modal) => void;
20 | }
21 |
22 | /** Builds a modal panel, which can be triggered using its id */
23 | export const ModalPanel: FactoryComponent = () => ({
24 | oncreate: ({ dom, attrs: { options, onCreate } }) => {
25 | const modal = M.Modal.init(dom, options);
26 | if (onCreate) {
27 | onCreate(modal);
28 | }
29 | },
30 | view: ({ attrs: { id, title, description, fixedFooter, bottomSheet, buttons, richContent, className } }) => {
31 | const cn = [className, fixedFooter ? 'modal-fixed-footer' : '', bottomSheet ? 'bottom-sheet' : '']
32 | .filter(Boolean)
33 | .join(' ')
34 | .trim();
35 | return m(
36 | '.modal',
37 | {
38 | id,
39 | className: cn,
40 | },
41 | [
42 | m('.modal-content', [
43 | m('h4', title),
44 | richContent && typeof description === 'string'
45 | ? m.trust(description || '')
46 | : typeof description === 'string'
47 | ? m('p', description)
48 | : description,
49 | ]),
50 | buttons
51 | ? m(
52 | '.modal-footer',
53 | buttons.map((props) => m(FlatButton, { ...props, className: 'modal-close' }))
54 | )
55 | : undefined,
56 | ]
57 | );
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/packages/lib/src/option.ts:
--------------------------------------------------------------------------------
1 | import m, { Vnode, FactoryComponent, Attributes, Component } from 'mithril';
2 | import { Label, HelperText } from './label';
3 |
4 | export interface IInputCheckbox extends Attributes {
5 | /** Optional event handler when a checkbox is clicked */
6 | onchange?: (checked: boolean) => void;
7 | /** Label of the checkbox, can be a string or Vnode */
8 | label?: string | Vnode;
9 | /** If true, the checkbox is checked */
10 | checked?: boolean;
11 | /** If true, the checkbox is disabled */
12 | disabled?: boolean;
13 | }
14 |
15 | /** Component to show a check box */
16 | export const InputCheckbox: FactoryComponent = () => {
17 | return {
18 | view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style } }) => {
19 | return m(
20 | `div`,
21 | { className, style },
22 | m('label', [
23 | m('input[type=checkbox][tabindex=0]', {
24 | checked,
25 | disabled,
26 | onclick: onchange
27 | ? (e: Event) => {
28 | if (e.target && typeof (e.target as HTMLInputElement).checked !== 'undefined') {
29 | onchange((e.target as HTMLInputElement).checked);
30 | }
31 | }
32 | : undefined,
33 | }),
34 | label ? (typeof label === 'string' ? m('span', label) : label) : undefined,
35 | ]),
36 | description && m(HelperText, { className: 'input-checkbox-desc', helperText: description })
37 | );
38 | },
39 | };
40 | };
41 |
42 | export interface IInputOption {
43 | /** Option ID */
44 | id: T;
45 | /** Displayed label */
46 | label: string;
47 | /** Optional title, often used to display a tooltip - will only work when choosing browser-defaults */
48 | title?: string;
49 | /** Is the option disabled? */
50 | disabled?: boolean;
51 | /** Select image */
52 | img?: string;
53 | /** Select group label */
54 | group?: string;
55 | /** Optional class name */
56 | className?: string;
57 | /** Optional description */
58 | description?: string;
59 | }
60 |
61 | export interface IOptions extends Attributes {
62 | /** Element ID */
63 | id?: string;
64 | /** Optional title or label */
65 | label?: string;
66 | /** The options that you have */
67 | options: IInputOption[];
68 | /** Event handler that is called when an option is changed */
69 | onchange?: (checkedId: T[]) => void;
70 | /**
71 | * Selected id or ids (in case of multiple options)
72 | * @deprecated Please use initialValue instead
73 | */
74 | checkedId?: T | T[];
75 | /** Selected id or ids (in case of multiple options) */
76 | initialValue?: T | T[];
77 | /** Optional description */
78 | description?: string;
79 | /** Optional CSS that is added to the input checkbox, e.g. if you add col s4, the items will be put inline */
80 | checkboxClass?: string;
81 | /** If true, start on a new row */
82 | newRow?: boolean;
83 | /** If true, add a mandatory '*' after the label */
84 | isMandatory?: boolean;
85 | /** If true, disable the options. */
86 | disabled?: boolean;
87 | }
88 |
89 | /** A list of checkboxes */
90 | export const Options = (): Component> => {
91 | const state = {} as {
92 | checkedId?: T | T[];
93 | checkedIds: T[];
94 | };
95 |
96 | const isChecked = (id: T) => state.checkedIds.indexOf(id) >= 0;
97 |
98 | return {
99 | oninit: ({ attrs: { initialValue, checkedId } }) => {
100 | const iv = checkedId || initialValue;
101 | state.checkedId = checkedId;
102 | state.checkedIds = iv ? (iv instanceof Array ? [...iv] : [iv]) : [];
103 | },
104 | view: ({
105 | attrs: {
106 | label,
107 | id,
108 | options,
109 | checkedId,
110 | description,
111 | className = 'col s12',
112 | style,
113 | disabled,
114 | checkboxClass,
115 | newRow,
116 | isMandatory,
117 | onchange: callback,
118 | },
119 | }) => {
120 | if (checkedId && state.checkedId !== checkedId) {
121 | state.checkedId = checkedId;
122 | state.checkedIds = checkedId instanceof Array ? checkedId : [checkedId];
123 | }
124 | const onchange = callback
125 | ? (propId: T, checked: boolean) => {
126 | const checkedIds = state.checkedIds.filter((i) => i !== propId);
127 | if (checked) {
128 | checkedIds.push(propId);
129 | }
130 | state.checkedIds = checkedIds;
131 | callback(checkedIds);
132 | }
133 | : undefined;
134 | const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim();
135 | return m('div', { className: cn, style }, [
136 | m('div', { className: 'input-field options' }, m(Label, { id, label, isMandatory })),
137 | m(HelperText, { helperText: description }),
138 | ...options.map((option) =>
139 | m(InputCheckbox, {
140 | disabled: disabled || option.disabled,
141 | label: option.label,
142 | onchange: onchange ? (v: boolean) => onchange(option.id, v) : undefined,
143 | className: option.className || checkboxClass,
144 | checked: isChecked(option.id),
145 | description: option.description,
146 | })
147 | ),
148 | ]);
149 | },
150 | };
151 | };
152 |
--------------------------------------------------------------------------------
/packages/lib/src/pagination.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes, Vnode } from 'mithril';
2 |
3 | export interface IInternalPaginationOption extends IPaginationOption {
4 | active?: boolean;
5 | title: number | Vnode;
6 | }
7 |
8 | export interface IPaginationOption extends Attributes {
9 | href: string;
10 | disabled?: boolean;
11 | }
12 |
13 | const PaginationItem: FactoryComponent = () => ({
14 | view: ({ attrs: { title, href, active, disabled } }) =>
15 | m(
16 | 'li',
17 | { className: active ? 'active' : disabled ? 'disabled' : 'waves-effect' },
18 | typeof title === 'number' ? m(m.route.Link, { href }, title) : title
19 | ),
20 | });
21 |
22 | export interface IPaginationOptions extends Attributes {
23 | /**
24 | * How many items do we show
25 | * @default 9 or items.length, whatever is the smallest
26 | */
27 | size?: number;
28 | /** The active page index */
29 | curPage?: number;
30 | items: IPaginationOption[];
31 | }
32 |
33 | export const Pagination: FactoryComponent = () => {
34 | const state = {
35 | pagIndex: 0,
36 | };
37 | return {
38 | view: ({ attrs: { items, curPage = 1, size = Math.min(9, items.length) } }) => {
39 | const { pagIndex } = state;
40 | const startPage = pagIndex * size;
41 | const endPage = startPage + size;
42 | const canGoBack = pagIndex > 0;
43 | const canGoForward = endPage < items.length;
44 | const displayedItems = [
45 | {
46 | title: m(
47 | 'a',
48 | {
49 | onclick: () => canGoBack && state.pagIndex--,
50 | },
51 | m('i.material-icons', 'chevron_left')
52 | ),
53 | disabled: !canGoBack,
54 | },
55 | ...items.filter((_, i) => startPage <= i && i < endPage),
56 | {
57 | title: m(
58 | 'a',
59 | {
60 | onclick: () => canGoForward && state.pagIndex++,
61 | },
62 | m('i.material-icons', 'chevron_right')
63 | ),
64 | disabled: !canGoForward,
65 | },
66 | ] as (
67 | | IPaginationOption
68 | | {
69 | title?: m.Vnode;
70 | disabled: boolean;
71 | }
72 | )[];
73 | return m(
74 | 'ul.pagination',
75 | displayedItems.map((item, i) =>
76 | m(PaginationItem, {
77 | title: startPage + i,
78 | ...item,
79 | active: startPage + i === curPage,
80 | })
81 | )
82 | );
83 | },
84 | };
85 | };
86 |
--------------------------------------------------------------------------------
/packages/lib/src/parallax.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent, Attributes } from 'mithril';
2 |
3 | export interface IParallax extends Partial, Attributes {
4 | /** Image source */
5 | src: string;
6 | }
7 |
8 | /**
9 | * Parallax component:
10 | * Parallax is an effect where the background content or image in this case,
11 | * is moved at a different speed than the foreground content while scrolling.
12 | * @see https://materializecss.com/parallax.html
13 | */
14 | export const Parallax: FactoryComponent = () => {
15 | return {
16 | oncreate: ({ dom, attrs }) => {
17 | M.Parallax.init(dom, attrs);
18 | },
19 | view: ({ attrs: { src } }) => (src ? m('.parallax-container', m('.parallax', m('img', { src }))) : undefined),
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/packages/lib/src/pickers.ts:
--------------------------------------------------------------------------------
1 | import m, { FactoryComponent } from 'mithril';
2 | import { IInputOptions } from './input-options';
3 | import { uniqueId } from './utils';
4 | import { Label, HelperText } from './label';
5 |
6 | /** Component to pick a date */
7 | export const DatePicker: FactoryComponent & Partial> = () => {
8 | const state = { id: uniqueId() } as { id: string; dp: M.Datepicker };
9 | return {
10 | view: ({
11 | attrs: {
12 | label,
13 | helperText,
14 | initialValue,
15 | newRow,
16 | className = 'col s12',
17 | iconName,
18 | isMandatory,
19 | onchange,
20 | disabled,
21 | ...props
22 | },
23 | }) => {
24 | const id = state.id;
25 | // const attributes = toAttrs(props);
26 | const onClose = onchange ? () => state.dp && onchange(state.dp.date) : undefined;
27 | const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim();
28 | return m(
29 | '.input-field',
30 | {
31 | className: cn,
32 | onremove: () => {
33 | return state.dp && state.dp.destroy();
34 | },
35 | },
36 | [
37 | iconName ? m('i.material-icons.prefix', iconName) : '',
38 | m('input', {
39 | ...props,
40 | type: 'text',
41 | tabindex: 0,
42 | className: 'datepicker',
43 | id,
44 | // attributes,
45 | disabled,
46 | oncreate: ({ dom }) => {
47 | state.dp = M.Datepicker.init(dom, {
48 | format: 'yyyy/mm/dd',
49 | showClearBtn: true,
50 | setDefaultDate: true,
51 | defaultDate: initialValue ? new Date(initialValue) : new Date(),
52 | // onSelect: onchange,
53 | ...props,
54 | onClose,
55 | } as Partial);
56 | },
57 | }),
58 | m(Label, { label, id, isMandatory, isActive: !!initialValue }),
59 | m(HelperText, { helperText }),
60 | ]
61 | );
62 | },
63 | };
64 | };
65 |
66 | /** Component to pick a time */
67 | export const TimePicker: FactoryComponent> = () => {
68 | const state = { id: uniqueId() } as { id: string; tp: M.Timepicker };
69 | return {
70 | view: ({
71 | attrs: {
72 | label,
73 | helperText,
74 | initialValue,
75 | newRow,
76 | className = 'col s12',
77 | iconName,
78 | isMandatory,
79 | onchange,
80 | disabled,
81 | ...props
82 | },
83 | }) => {
84 | const id = state.id;
85 | // const attributes = toAttrs(props);
86 | const now = new Date();
87 | const onCloseEnd = onchange
88 | ? () => state.tp && onchange(state.tp.time || initialValue || `${now.getHours()}:${now.getMinutes()}`)
89 | : undefined;
90 | const cn = ['input-field', 'timepicker', newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim();
91 | return m(
92 | 'div',
93 | {
94 | className: cn,
95 | onremove: () => state.tp && state.tp.destroy(),
96 | },
97 | [
98 | iconName ? m('i.material-icons.prefix', iconName) : '',
99 | m('input', {
100 | ...props,
101 | type: 'text',
102 | tabindex: 0,
103 | id,
104 | disabled,
105 | value: initialValue,
106 | oncreate: ({ dom }) => {
107 | state.tp = M.Timepicker.init(dom, {
108 | twelveHour: false,
109 | showClearBtn: true,
110 | defaultTime: initialValue,
111 | // onSelect: onchange ? (hours: number, minutes: number) => onchange(`${hours}:${minutes}`) : undefined,
112 | ...props,
113 | onCloseEnd,
114 | } as Partial);
115 | },
116 | }),
117 | m(Label, { label, id, isMandatory, isActive: initialValue }),
118 | m(HelperText, { helperText }),
119 | ]
120 | );
121 | },
122 | };
123 | };
124 |
--------------------------------------------------------------------------------
/packages/lib/src/radio.ts:
--------------------------------------------------------------------------------
1 | import m, { Attributes, Component } from 'mithril';
2 | import { uniqueId } from './utils';
3 | import { IInputOption } from './option';
4 | import { Label } from './label';
5 |
6 | export interface IRadioButtons extends Attributes {
7 | /** Element ID */
8 | id?: string;
9 | /** Optional title or label */
10 | label?: string;
11 | /** The options that you have */
12 | options: IInputOption[];
13 | /** Event handler that is called when an option is changed */
14 | onchange: (id: T) => void;
15 | /** Selected id (in oninit lifecycle) */
16 | initialValue?: T;
17 | /** Selected id (in oninit and onupdate lifecycle) */
18 | checkedId?: T;
19 | /** Optional description */
20 | description?: string;
21 | /** If true, start on a new row */
22 | newRow?: boolean;
23 | /** If true, add a mandatory '*' after the label */
24 | isMandatory?: boolean;
25 | /** Optional CSS that is added to the input checkbox, e.g. if you add col s4, the items will be put inline */
26 | checkboxClass?: string;
27 | /** Disable the button */
28 | disabled?: boolean;
29 | }
30 |
31 | export interface IRadioButton extends Attributes {
32 | id: T;
33 | checked?: boolean;
34 | onchange: (id: T) => void;
35 | label: string;
36 | groupId: string;
37 | disabled?: boolean;
38 | }
39 |
40 | export const RadioButton = (): Component> => ({
41 | view: ({ attrs: { id, groupId, label, onchange, className = 'col s12', checked, disabled } }) => {
42 | return m(
43 | 'div',
44 | { className },
45 | m('label', [
46 | m('input[type=radio][tabindex=0]', {
47 | name: groupId,
48 | disabled,
49 | checked,
50 | onclick: onchange ? () => onchange(id) : undefined,
51 | }),
52 | m('span', m.trust(label)),
53 | ])
54 | );
55 | },
56 | });
57 |
58 | /** Component to show a list of radio buttons, from which you can choose one. */
59 | // export const RadioButtons: FactoryComponent> = () => {
60 | export const RadioButtons = (): Component> => {
61 | const state = { groupId: uniqueId() } as {
62 | groupId: string;
63 | oldCheckedId?: T;
64 | checkedId?: T;
65 | onchange: (id: T) => void;
66 | };
67 | return {
68 | oninit: ({ attrs: { checkedId, initialValue } }) => {
69 | state.oldCheckedId = checkedId;
70 | state.checkedId = checkedId || initialValue;
71 | },
72 | view: ({
73 | attrs: {
74 | id,
75 | checkedId: cid,
76 | newRow,
77 | className = 'col s12',
78 | label = '',
79 | disabled,
80 | description,
81 | options,
82 | isMandatory,
83 | checkboxClass,
84 | onchange: callback,
85 | },
86 | }) => {
87 | if (state.oldCheckedId !== cid) {
88 | state.oldCheckedId = state.checkedId = cid;
89 | }
90 | const { groupId, checkedId } = state;
91 | const onchange = (propId: T) => {
92 | state.checkedId = propId;
93 | if (callback) {
94 | callback(propId);
95 | }
96 | };
97 |
98 | if (newRow) className += ' clear';
99 | return m('div', { id, className }, [
100 | m('div', { className: 'input-field options' }, m(Label, { id, label, isMandatory })),
101 | description ? m('p.helper-text', m.trust(description)) : '',
102 | ...options.map((r) =>
103 | m(RadioButton, {
104 | ...r,
105 | onchange,
106 | groupId,
107 | disabled,
108 | className: checkboxClass,
109 | checked: r.id === checkedId,
110 | } as IRadioButton)
111 | ),
112 | ]);
113 | },
114 | };
115 | };
116 |
--------------------------------------------------------------------------------
/packages/lib/src/search-select.ts:
--------------------------------------------------------------------------------
1 | import m, { Attributes, Component } from 'mithril';
2 |
3 | // Option interface for type safety
4 | export interface Option {
5 | id: T;
6 | label?: string;
7 | disabled?: boolean;
8 | }
9 |
10 | // Component attributes interface
11 | export interface SearchSelectAttrs extends Attributes {
12 | /** Options to display in the select */
13 | options?: Option[];
14 | /** Initial value */
15 | initialValue?: T[];
16 | /** Callback when user selects or deselects an option */
17 | onchange?: (selectedOptions: T[]) => void | Promise;
18 | /** Callback when user creates a new option: should return new ID */
19 | oncreateNewOption?: (term: string) => Option | Promise