mirror of
https://github.com/vale981/Vulcan
synced 2025-03-05 09:31:43 -05:00
material-ui
This commit is contained in:
parent
448f9c16b8
commit
a743022545
90 changed files with 7582 additions and 22 deletions
|
@ -55,7 +55,7 @@
|
|||
"avoid-escape"
|
||||
],
|
||||
"react/prop-types": 0,
|
||||
"semi": [1, "always"]
|
||||
"semi": [2, "always"]
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
|
|
64
package-lock.json
generated
64
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Vulcan",
|
||||
"version": "1.12.16",
|
||||
"version": "1.12.17",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -702,7 +702,6 @@
|
|||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
|
||||
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2",
|
||||
"longest": "^1.0.1",
|
||||
|
@ -898,11 +897,12 @@
|
|||
}
|
||||
},
|
||||
"apollo-errors": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.5.1.tgz",
|
||||
"integrity": "sha512-gYAceMzNJfF+mUHH2/4UcZTkZtDY54arCTKGbKa7WU5IXnTJ4V+P94wHodcDkLLHWpHL8SW1hEgjN5ZINcPb1w==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.9.0.tgz",
|
||||
"integrity": "sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==",
|
||||
"requires": {
|
||||
"es6-error": "^4.0.0"
|
||||
"assert": "^1.4.1",
|
||||
"extendable-error": "^0.1.5"
|
||||
}
|
||||
},
|
||||
"apollo-link": {
|
||||
|
@ -1289,6 +1289,14 @@
|
|||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"assert": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
|
||||
"integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
|
||||
"requires": {
|
||||
"util": "0.10.3"
|
||||
}
|
||||
},
|
||||
"assert-err": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-err/-/assert-err-1.1.0.tgz",
|
||||
|
@ -2384,7 +2392,7 @@
|
|||
},
|
||||
"cheerio": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
|
||||
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
|
||||
"requires": {
|
||||
"css-select": "~1.2.0",
|
||||
|
@ -2831,7 +2839,7 @@
|
|||
},
|
||||
"css-select": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
||||
"requires": {
|
||||
"boolbase": "~1.0.0",
|
||||
|
@ -3370,11 +3378,6 @@
|
|||
"es6-symbol": "~3.1.1"
|
||||
}
|
||||
},
|
||||
"es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
|
||||
},
|
||||
"es6-iterator": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||
|
@ -3977,6 +3980,11 @@
|
|||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
|
||||
},
|
||||
"extendable-error": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.5.tgz",
|
||||
"integrity": "sha1-EiMIpwl7yJomOyxPvwiceBQOO20="
|
||||
},
|
||||
"extglob": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
|
||||
|
@ -6168,8 +6176,7 @@
|
|||
"longest": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
|
||||
"optional": true
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.3.1",
|
||||
|
@ -6261,7 +6268,7 @@
|
|||
"process": "^0.11.9",
|
||||
"punycode": "^1.4.1",
|
||||
"querystring-es3": "^0.2.1",
|
||||
"readable-stream": "git+https://github.com/meteor/readable-stream.git",
|
||||
"readable-stream": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
|
||||
"stream-browserify": "^2.0.1",
|
||||
"string_decoder": "^1.0.1",
|
||||
"timers-browserify": "^1.4.2",
|
||||
|
@ -6711,11 +6718,11 @@
|
|||
"version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12",
|
||||
"from": "git+https://github.com/meteor/readable-stream.git",
|
||||
"requires": {
|
||||
"inherits": "~2.0.3",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.0",
|
||||
"process-nextick-args": "~1.0.6",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"string_decoder": "~1.0.0",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
|
@ -9717,6 +9724,21 @@
|
|||
"os-homedir": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
"requires": {
|
||||
"inherits": "2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
|
||||
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
|
||||
}
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -9852,7 +9874,7 @@
|
|||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
||||
"requires": {
|
||||
"ansi-styles": "^2.2.1",
|
||||
|
|
15
packages/vulcan-ui-material/.editorconfig
Normal file
15
packages/vulcan-ui-material/.editorconfig
Normal file
|
@ -0,0 +1,15 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.js]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
86
packages/vulcan-ui-material/.eslintrc
Executable file
86
packages/vulcan-ui-material/.eslintrc
Executable file
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
//"airbnb",
|
||||
"plugin:meteor/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"allowImportExportEverywhere": true,
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"babel/generator-star-spacing": 0,
|
||||
"babel/new-cap": [1, {
|
||||
"capIsNewExceptions": [
|
||||
"Optional",
|
||||
"OneOf",
|
||||
"Maybe",
|
||||
"MailChimpAPI",
|
||||
"Juice",
|
||||
"Run",
|
||||
"AppComposer",
|
||||
"Query",
|
||||
"InArray"
|
||||
]
|
||||
}],
|
||||
"babel/array-bracket-spacing": 0,
|
||||
"babel/object-curly-spacing": 0,
|
||||
"babel/object-shorthand": 0,
|
||||
"babel/arrow-parens": 0,
|
||||
"babel/no-await-in-loop": 1,
|
||||
"comma-dangle": 0,
|
||||
"key-spacing": 0,
|
||||
"no-extra-boolean-cast": 0,
|
||||
"no-undef": 1,
|
||||
"no-unused-vars": [1, {
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "React|PropTypes|Component"
|
||||
}],
|
||||
"react/prop-types": 0,
|
||||
"react/display-name": 0,
|
||||
"meteor/audit-argument-checks": 0,
|
||||
"meteor/no-session": 0,
|
||||
"no-case-declarations": 0,
|
||||
"no-console": 0,
|
||||
"semi": "error",
|
||||
"quotes": [
|
||||
1,
|
||||
"single"
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
"meteor": true,
|
||||
"node": true
|
||||
},
|
||||
"plugins": [
|
||||
"babel",
|
||||
"meteor",
|
||||
"react",
|
||||
"jsx-a11y"
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"meteor": {
|
||||
"paths": [
|
||||
"/usr/local/share/global_modules"
|
||||
],
|
||||
"moduleDirectory": [
|
||||
"node_modules",
|
||||
"packages"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": true,
|
||||
"globals": {
|
||||
"param": true,
|
||||
"returns": true
|
||||
}
|
||||
}
|
3
packages/vulcan-ui-material/.gitignore
vendored
Normal file
3
packages/vulcan-ui-material/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
npm-debug.log
|
||||
node_modules
|
||||
.idea/workspace.xml
|
89
packages/vulcan-ui-material/.versions
Normal file
89
packages/vulcan-ui-material/.versions
Normal file
|
@ -0,0 +1,89 @@
|
|||
accounts-base@1.4.3
|
||||
allow-deny@1.1.0
|
||||
autoupdate@1.5.0
|
||||
babel-compiler@7.2.4
|
||||
babel-runtime@1.3.0
|
||||
base64@1.0.11
|
||||
binary-heap@1.0.11
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.6.0
|
||||
buffer@0.0.0
|
||||
caching-compiler@1.2.1
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.1.0
|
||||
check@1.3.1
|
||||
ddp@1.4.0
|
||||
ddp-client@2.3.3
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.2.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.5.0
|
||||
ecmascript@0.12.4
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.8.0
|
||||
ecmascript-runtime-server@0.7.1
|
||||
ejson@1.1.0
|
||||
email@1.2.3
|
||||
erikdakoda:vulcan-material-ui@1.12.8_17
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.0
|
||||
fourseven:scss@4.10.0
|
||||
geojson-utils@1.0.10
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.4.1
|
||||
id-map@1.1.0
|
||||
inter-process-messaging@0.1.0
|
||||
localstorage@1.2.0
|
||||
logging@1.1.20
|
||||
meteor@1.9.2
|
||||
meteorhacks:inject-initial@1.0.4
|
||||
meteorhacks:picker@1.0.3
|
||||
minifier-css@1.4.1
|
||||
minifier-js@2.4.0
|
||||
minimongo@1.4.5
|
||||
modern-browsers@0.1.3
|
||||
modules@0.13.0
|
||||
modules-runtime@0.10.3
|
||||
mongo@1.6.0
|
||||
mongo-decimal@0.1.0
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
npm-mongo@3.1.1
|
||||
ordered-dict@1.1.0
|
||||
percolatestudio:synced-cron@1.1.0
|
||||
promise@0.11.2
|
||||
random@1.1.0
|
||||
rate-limit@1.0.9
|
||||
reactive-dict@1.2.1
|
||||
reactive-var@1.0.11
|
||||
reload@1.2.0
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.0
|
||||
server-render@0.3.1
|
||||
service-configuration@1.0.11
|
||||
session@1.2.0
|
||||
shell-server@0.4.0
|
||||
socket-stream-client@0.2.2
|
||||
spacebars-compiler@1.1.3
|
||||
standard-minifier-css@1.5.2
|
||||
standard-minifier-js@2.4.0
|
||||
static-html@1.2.2
|
||||
templating-tools@1.1.2
|
||||
tracker@1.2.0
|
||||
underscore@1.0.10
|
||||
url@1.2.0
|
||||
vulcan:accounts@1.12.8
|
||||
vulcan:core@1.12.8
|
||||
vulcan:debug@1.12.8
|
||||
vulcan:email@1.12.8
|
||||
vulcan:forms@1.12.8
|
||||
vulcan:i18n@1.12.8
|
||||
vulcan:lib@1.12.8
|
||||
vulcan:routing@1.12.8
|
||||
vulcan:users@1.12.8
|
||||
webapp@1.7.1
|
||||
webapp-hashing@1.0.9
|
18
packages/vulcan-ui-material/accounts.css
Normal file
18
packages/vulcan-ui-material/accounts.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
.accounts-ui .form-control {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
padding: 8px 0;
|
||||
font-size: 1rem;
|
||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.87);
|
||||
margin-bottom: 1px;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.accounts-ui .form-control:focus {
|
||||
border-bottom-width: 2px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
3
packages/vulcan-ui-material/client/main.js
Normal file
3
packages/vulcan-ui-material/client/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from '../components/index';
|
||||
export * from '../modules/index';
|
||||
import './wrapWithMuiTheme';
|
29
packages/vulcan-ui-material/client/wrapWithMuiTheme.jsx
Normal file
29
packages/vulcan-ui-material/client/wrapWithMuiTheme.jsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { addCallback, registerComponent, Components } from 'meteor/vulcan:core';
|
||||
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
|
||||
import { getCurrentTheme } from '../modules/themes';
|
||||
import JssCleanup from '../components/theme/JssCleanup';
|
||||
|
||||
class ThemeProvider extends React.Component {
|
||||
render() {
|
||||
const theme = getCurrentTheme();
|
||||
return (
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<JssCleanup>{this.props.children}</JssCleanup>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerComponent('ThemeProvider', ThemeProvider);
|
||||
|
||||
function wrapWithMuiTheme(app) {
|
||||
return (
|
||||
<Components.ThemeProvider>
|
||||
{app}
|
||||
</Components.ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
addCallback('router.client.wrapper', wrapWithMuiTheme);
|
46
packages/vulcan-ui-material/components/accounts/AccountsButton.jsx
Executable file
46
packages/vulcan-ui-material/components/accounts/AccountsButton.jsx
Executable file
|
@ -0,0 +1,46 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { replaceComponent, Utils } from 'meteor/vulcan:core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
export class AccountsButton extends Component {
|
||||
render () {
|
||||
|
||||
const {
|
||||
label,
|
||||
type,
|
||||
disabled = false,
|
||||
className,
|
||||
onClick
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={type === 'link' ? 'text' : 'contained'}
|
||||
size={type === 'link' ? 'small' : undefined}
|
||||
color="primary"
|
||||
className={classNames(`button-${Utils.slugify(label)}`, className)}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
disableRipple={true}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AccountsButton.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['link', 'submit', 'button']),
|
||||
disabled: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('AccountsButton', AccountsButton);
|
48
packages/vulcan-ui-material/components/accounts/AccountsButtons.jsx
Executable file
48
packages/vulcan-ui-material/components/accounts/AccountsButtons.jsx
Executable file
|
@ -0,0 +1,48 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexDirection: 'row-reverse',
|
||||
padding: theme.spacing.unit * 2,
|
||||
height: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export class AccountsButtons extends Component {
|
||||
render () {
|
||||
|
||||
const {
|
||||
classes,
|
||||
buttons = {},
|
||||
className = 'buttons',
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<CardActions className={classNames(classes.root, className)}>
|
||||
{Object.keys(buttons).map((id, i) =>
|
||||
<Components.AccountsButton {...buttons[id]} key={i}/>
|
||||
)}
|
||||
</CardActions>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AccountsButtons.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
buttons: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
AccountsButtons.displayName = 'AccountsButtons';
|
||||
|
||||
|
||||
replaceComponent('AccountsButtons', AccountsButtons, [withStyles, styles]);
|
114
packages/vulcan-ui-material/components/accounts/AccountsField.jsx
Executable file
114
packages/vulcan-ui-material/components/accounts/AccountsField.jsx
Executable file
|
@ -0,0 +1,114 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { replaceComponent } from 'meteor/vulcan:core';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
|
||||
|
||||
const autocompleteValues = {
|
||||
'username': 'username',
|
||||
'usernameOrEmail': 'email',
|
||||
'email': 'email',
|
||||
'password': 'current-password'
|
||||
};
|
||||
|
||||
|
||||
export class AccountsField extends PureComponent {
|
||||
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mount: true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
triggerUpdate () {
|
||||
// Trigger an onChange on initial load, to support browser pre-filled values.
|
||||
const { onChange } = this.props;
|
||||
if (this.input && onChange) {
|
||||
onChange({ target: { value: this.input.value } });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount () {
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
// Re-mount component so that we don't expose browser pre-filled passwords if the component was
|
||||
// a password before and now something else.
|
||||
if (prevProps.id !== this.props.id) {
|
||||
this.setState({ mount: false });
|
||||
} else if (!this.state.mount) {
|
||||
this.setState({ mount: true });
|
||||
this.triggerUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render () {
|
||||
const {
|
||||
id,
|
||||
hint,
|
||||
label,
|
||||
type = 'text',
|
||||
onChange,
|
||||
required = false,
|
||||
className = 'field',
|
||||
defaultValue = '',
|
||||
autoFocus,
|
||||
messages,
|
||||
} = this.props;
|
||||
let { message } = this.props;
|
||||
const { mount = true } = this.state;
|
||||
|
||||
if (type === 'notice') {
|
||||
return <div className={className}>{label}</div>;
|
||||
}
|
||||
|
||||
const autoComplete = autocompleteValues[id];
|
||||
|
||||
if (messages && messages.find && typeof id === 'string') {
|
||||
const foundMessage = messages.find(element => {
|
||||
if (typeof element.field !== 'string') return false;
|
||||
return id.toLowerCase().indexOf(element.field.toLowerCase()) > -1;
|
||||
});
|
||||
if (foundMessage) {
|
||||
message = foundMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
mount &&
|
||||
|
||||
<div className={className} style={{ marginBottom: '10px' }}>
|
||||
<TextField
|
||||
id={id}
|
||||
type={type}
|
||||
inputRef={ref => { this.input = ref; }}
|
||||
onChange={onChange}
|
||||
placeholder={hint}
|
||||
defaultValue={defaultValue}
|
||||
autoComplete={autoComplete }
|
||||
label={label}
|
||||
autoFocus={autoFocus}
|
||||
required={required}
|
||||
error={!!message}
|
||||
helperText={message && message.message}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AccountsField.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('AccountsField', AccountsField);
|
27
packages/vulcan-ui-material/components/accounts/AccountsFields.jsx
Executable file
27
packages/vulcan-ui-material/components/accounts/AccountsFields.jsx
Executable file
|
@ -0,0 +1,27 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
|
||||
|
||||
export class AccountsFields extends Component {
|
||||
render () {
|
||||
const {
|
||||
fields = {},
|
||||
className = 'fields',
|
||||
messages,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<CardContent className={className}>
|
||||
{
|
||||
Object.keys(fields).map((id, i) =>
|
||||
<Components.AccountsField {...fields[id]} messages={messages} autoFocus={i === 0} key={i}/>
|
||||
)
|
||||
}
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
replaceComponent('AccountsFields', AccountsFields);
|
68
packages/vulcan-ui-material/components/accounts/AccountsForm.jsx
Executable file
68
packages/vulcan-ui-material/components/accounts/AccountsForm.jsx
Executable file
|
@ -0,0 +1,68 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
messages: theme.utils.errorMessage,
|
||||
});
|
||||
|
||||
|
||||
export class AccountsForm extends Component {
|
||||
|
||||
|
||||
componentDidMount () {
|
||||
let form = this.form;
|
||||
if (form) {
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render () {
|
||||
const {
|
||||
oauthServices,
|
||||
fields,
|
||||
buttons,
|
||||
messages,
|
||||
ready = true,
|
||||
className,
|
||||
classes,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<form ref={(ref) => this.form = ref}
|
||||
className={classNames(className, 'accounts-ui', { 'ready': ready, })}
|
||||
noValidate
|
||||
>
|
||||
<Components.AccountsFields fields={fields} messages={messages}/>
|
||||
<Components.AccountsButtons buttons={{...buttons}}/>
|
||||
<Components.AccountsPasswordOrService oauthServices={oauthServices}/>
|
||||
<Components.AccountsSocialButtons oauthServices={oauthServices}/>
|
||||
<Components.AccountsFormMessages messages={messages} className={classes.messages}/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
AccountsForm.propTypes = {
|
||||
oauthServices: PropTypes.object,
|
||||
fields: PropTypes.object.isRequired,
|
||||
buttons: PropTypes.object.isRequired,
|
||||
error: PropTypes.string,
|
||||
ready: PropTypes.bool,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
AccountsForm.displayName = 'AccountsForm';
|
||||
|
||||
|
||||
registerComponent('AccountsForm', AccountsForm, [withStyles, styles]);
|
|
@ -0,0 +1,57 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { replaceComponent } from 'meteor/vulcan:core';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexDirection: 'row-reverse',
|
||||
paddingRight: theme.spacing.unit * 2,
|
||||
paddingLeft: theme.spacing.unit * 2,
|
||||
height: 'auto',
|
||||
},
|
||||
typography: {
|
||||
marginRight: theme.spacing.unit,
|
||||
}
|
||||
});
|
||||
|
||||
export function hasPasswordService() {
|
||||
// First look for OAuth services.
|
||||
return !!Package['accounts-password'];
|
||||
}
|
||||
|
||||
export class AccountsPasswordOrService extends PureComponent {
|
||||
render () {
|
||||
let { className = 'password-or-service', style = {}, classes } = this.props;
|
||||
const services = Object.keys(this.props.oauthServices).map(service => {
|
||||
return this.props.oauthServices[service].label;
|
||||
});
|
||||
let labels = services;
|
||||
if (services.length > 2) {
|
||||
labels = [];
|
||||
}
|
||||
|
||||
if (hasPasswordService() && services.length > 0) {
|
||||
return (<CardActions className={classNames(className, classes.root)}>
|
||||
<Typography variant="caption" className={classes.typography} align="right">
|
||||
{ `${this.context.intl.formatMessage({id: 'accounts.or_use'})} ${ labels.join(' / ') }` }
|
||||
</Typography></CardActions>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
AccountsPasswordOrService.propTypes = {
|
||||
oauthServices: PropTypes.object
|
||||
};
|
||||
|
||||
AccountsPasswordOrService.contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
replaceComponent('AccountsPasswordOrService', AccountsPasswordOrService, [withStyles, styles]);
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||
import CardActions from '@material-ui/core/CardActions'
|
||||
import withStyles from '@material-ui/core/styles/withStyles'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing.unit * 2,
|
||||
height: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
export class AccountsSocialButtons extends React.Component {
|
||||
render() {
|
||||
let { oauthServices = {}, className = 'social-buttons', classes } = this.props;
|
||||
return(
|
||||
<CardActions className={classNames(classes.root, className)}>
|
||||
{Object.keys(oauthServices).map((id, i) => {
|
||||
return <Components.AccountsButton {...oauthServices[id]} key={i} />;
|
||||
})}
|
||||
</CardActions>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
replaceComponent('AccountsSocialButtons', AccountsSocialButtons, [withStyles, styles]);
|
133
packages/vulcan-ui-material/components/bonus/LoadMore.jsx
Normal file
133
packages/vulcan-ui-material/components/bonus/LoadMore.jsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, intlShape } from 'react-intl';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import ArrowDownIcon from 'mdi-material-ui/ArrowDown';
|
||||
import ScrollTrigger from './ScrollTrigger';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
|
||||
root: {
|
||||
textAlign: 'center',
|
||||
flexBasis: '100%',
|
||||
},
|
||||
|
||||
textButton: {
|
||||
marginTop: theme.spacing.unit * 2,
|
||||
},
|
||||
|
||||
iconButton: {},
|
||||
|
||||
caption: {
|
||||
marginTop: theme.spacing.unit * 3,
|
||||
paddingTop: theme.spacing.unit,
|
||||
paddingBottom: theme.spacing.unit,
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const LoadMore = ({
|
||||
classes,
|
||||
count,
|
||||
totalCount,
|
||||
loadMore,
|
||||
networkStatus,
|
||||
showCount,
|
||||
useTextButton,
|
||||
className,
|
||||
infiniteScroll,
|
||||
}, { intl }) => {
|
||||
|
||||
const isLoadingMore = networkStatus === 2;
|
||||
const loadMoreText = intl.formatMessage({ id: 'load_more.load_more' });
|
||||
const title = `${loadMoreText} (${count}/${totalCount})`;
|
||||
const hasMore = totalCount > count;
|
||||
const countValues = { count, totalCount };
|
||||
|
||||
const loadMoreButton = useTextButton
|
||||
?
|
||||
<Button className={classes.textButton} onClick={() => loadMore()}>
|
||||
{title}
|
||||
</Button>
|
||||
:
|
||||
<IconButton className={classes.iconButton} onClick={() => loadMore()}>
|
||||
<ArrowDownIcon/>
|
||||
</IconButton>;
|
||||
|
||||
return (
|
||||
<div className={classNames('load-more', classes.root, className)}>
|
||||
{
|
||||
showCount &&
|
||||
|
||||
<Typography variant="caption" className={classes.caption}>
|
||||
<FormattedMessage id={`load_more.${hasMore ? 'loaded_count' : 'loaded_all'}`} values={countValues}/>
|
||||
</Typography>
|
||||
}
|
||||
{
|
||||
isLoadingMore
|
||||
|
||||
?
|
||||
|
||||
<Components.Loading/>
|
||||
|
||||
:
|
||||
|
||||
hasMore
|
||||
|
||||
?
|
||||
|
||||
infiniteScroll
|
||||
|
||||
?
|
||||
|
||||
<ScrollTrigger onEnter={() => loadMore()}>
|
||||
{loadMoreButton}
|
||||
</ScrollTrigger>
|
||||
|
||||
:
|
||||
|
||||
loadMoreButton
|
||||
:
|
||||
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
LoadMore.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
count: PropTypes.number,
|
||||
totalCount: PropTypes.number,
|
||||
loadMore: PropTypes.func,
|
||||
networkStatus: PropTypes.number,
|
||||
showCount: PropTypes.bool,
|
||||
useTextButton: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
infiniteScroll: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
LoadMore.defaultProps = {
|
||||
showCount: true,
|
||||
};
|
||||
|
||||
|
||||
LoadMore.contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
|
||||
LoadMore.displayName = 'LoadMore';
|
||||
|
||||
|
||||
registerComponent('LoadMore', LoadMore, [withStyles, styles]);
|
||||
|
125
packages/vulcan-ui-material/components/bonus/ScrollTrigger.jsx
Normal file
125
packages/vulcan-ui-material/components/bonus/ScrollTrigger.jsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _throttle from 'lodash/throttle';
|
||||
|
||||
|
||||
class ScrollTrigger extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.onScroll = _throttle(this.onScroll.bind(this), 100, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
this.onResize = _throttle(this.onResize.bind(this), 100, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
this.inViewport = false;
|
||||
this.passive = this.supportsPassive ? { passive: true } : false;
|
||||
}
|
||||
|
||||
supportsPassive () {
|
||||
let supportsPassive = false;
|
||||
try {
|
||||
const opts = Object.defineProperty({}, 'passive', {
|
||||
get: function() {
|
||||
supportsPassive = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener("testPassive", null, opts);
|
||||
window.removeEventListener("testPassive", null, opts);
|
||||
//eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
return supportsPassive;
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.scroller = document.getElementById('main');
|
||||
this.scroller.addEventListener('resize', this.onResize, this.passive);
|
||||
this.scroller.addEventListener('scroll', this.onScroll, this.passive);
|
||||
|
||||
this.inViewport = false;
|
||||
|
||||
if (this.props.triggerOnLoad) {
|
||||
this.checkStatus();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (!this.scroller) return;
|
||||
this.scroller.removeEventListener('resize', this.onResize);
|
||||
this.scroller.removeEventListener('scroll', this.onScroll);
|
||||
this.scroller = null;
|
||||
}
|
||||
|
||||
onResize () {
|
||||
this.checkStatus();
|
||||
}
|
||||
|
||||
onScroll () {
|
||||
this.checkStatus();
|
||||
}
|
||||
|
||||
checkStatus () {
|
||||
if (!this.scroller) return;
|
||||
|
||||
const {
|
||||
onEnter,
|
||||
} = this.props;
|
||||
|
||||
//eslint-disable-next-line
|
||||
const element = ReactDOM.findDOMNode(this.element);
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const viewportEnd = this.scroller.clientHeight + this.props.preload;
|
||||
const inViewport = elementRect.top < viewportEnd;
|
||||
|
||||
if (inViewport) {
|
||||
if (!this.inViewport) {
|
||||
this.inViewport = true;
|
||||
|
||||
onEnter(this);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (this.inViewport) {
|
||||
this.inViewport = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
children,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div ref={(element) => {this.element = element;}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ScrollTrigger.propTypes = {
|
||||
scrollerId: PropTypes.string,
|
||||
triggerOnLoad: PropTypes.bool,
|
||||
preload: PropTypes.number,
|
||||
onEnter: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
ScrollTrigger.defaultProps = {
|
||||
scrollerId: 'main',
|
||||
preload: 1000,
|
||||
triggerOnLoad: true,
|
||||
onEnter: () => {},
|
||||
};
|
||||
|
||||
|
||||
export default ScrollTrigger;
|
240
packages/vulcan-ui-material/components/bonus/SearchInput.jsx
Normal file
240
packages/vulcan-ui-material/components/bonus/SearchInput.jsx
Normal file
|
@ -0,0 +1,240 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import SearchIcon from 'mdi-material-ui/Magnify';
|
||||
import ClearIcon from 'mdi-material-ui/CloseCircle';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import NoSsr from '@material-ui/core/NoSsr';
|
||||
import classNames from 'classnames';
|
||||
import _debounce from 'lodash/debounce';
|
||||
import KeyboardEventHandler from 'react-keyboard-event-handler';
|
||||
import autosizeInput from 'autosize-input';
|
||||
|
||||
const styles = theme => ({
|
||||
|
||||
'@global': {
|
||||
'input[type=text]::-ms-clear, input[type=text]::-ms-reveal':
|
||||
{
|
||||
display: 'none',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
'input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button':
|
||||
{ display: 'none' },
|
||||
'input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration':
|
||||
{ display: 'none' },
|
||||
},
|
||||
|
||||
root: {
|
||||
display: 'inline-flex',
|
||||
backgroundColor: theme.palette.common.faintBlack,
|
||||
borderRadius: 20,
|
||||
padding: 6,
|
||||
},
|
||||
|
||||
clear: {
|
||||
transition: theme.transitions.create('opacity,transform', {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
opacity: 0.65,
|
||||
width: 36,
|
||||
height: 36,
|
||||
margin: -6,
|
||||
marginLeft: 0,
|
||||
'& svg': {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
flexDirection: 'column',
|
||||
},
|
||||
|
||||
clearDense: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
margin: -4,
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
clearDisabled: {
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
|
||||
dense: {
|
||||
padding: 4,
|
||||
},
|
||||
|
||||
icon: {
|
||||
color: theme.palette.common.lightBlack,
|
||||
marginLeft: theme.spacing.unit,
|
||||
marginRight: theme.spacing.unit,
|
||||
},
|
||||
|
||||
input: {
|
||||
lineHeight: 1,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
marginBottom: 1,
|
||||
/*transition: theme.transitions.create('width', {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),*/
|
||||
minWidth: 130,
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
class SearchInput extends PureComponent {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: props.defaultValue || '',
|
||||
};
|
||||
|
||||
this.input = null;
|
||||
this.removeAutosize = null;
|
||||
this.triggerResize = null;
|
||||
|
||||
this.updateQuery = _debounce(this.updateQuery, 500);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (!document) return;
|
||||
const element = document.querySelector(`.search-input-${this.props.name} input[type=search]`);
|
||||
|
||||
// We have to patch into addEventListener because autosizeInput provides no way to trigger resize
|
||||
element._addEventListener = element.addEventListener;
|
||||
element.addEventListener = function(type, listener, useCapture) {
|
||||
if(useCapture === undefined)
|
||||
useCapture = false;
|
||||
this._addEventListener(type, listener, useCapture);
|
||||
this.triggerResize = listener;
|
||||
};
|
||||
|
||||
this.removeAutosize = autosizeInput(element);
|
||||
|
||||
this.triggerResize = element.triggerResize;
|
||||
element.addEventListener = element._addEventListener;
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.removeAutosize) {
|
||||
this.removeAutosize();
|
||||
}
|
||||
}
|
||||
|
||||
handleShortcutKeys = (key, event) => {
|
||||
switch (key) {
|
||||
case 's':
|
||||
this.focusInput();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'c':
|
||||
case 'esc':
|
||||
this.clearSearch(event, true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleFocus = () => {
|
||||
this.input.select();
|
||||
};
|
||||
|
||||
focusInput = (event) => {
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
clearSearch = (event, dontFocus) => {
|
||||
this.setState({ value: '' }, this.triggerResize);
|
||||
this.updateQuery('');
|
||||
|
||||
if (!dontFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
};
|
||||
|
||||
updateSearch = (event) => {
|
||||
const value = event.target.value;
|
||||
this.setState({ value: value });
|
||||
this.updateQuery(value);
|
||||
};
|
||||
|
||||
updateQuery = (value) => {
|
||||
this.props.updateQuery(value);
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
classes,
|
||||
className,
|
||||
dense,
|
||||
noShortcuts,
|
||||
name,
|
||||
} = this.props;
|
||||
|
||||
const searchIcon = <SearchIcon className={classes.icon} onClick={this.focusInput}/>;
|
||||
|
||||
const clearButton = <Components.TooltipIntl
|
||||
titleId="search.clear"
|
||||
icon={<ClearIcon/>}
|
||||
onClick={this.clearSearch}
|
||||
classes={{
|
||||
root: classNames(!this.state.value && classes.clearDisabled),
|
||||
button: classNames('clear-button', classes.clear, dense && classes.clearDense),
|
||||
}}
|
||||
disabled={!this.state.value}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Input className={classNames('search-input', `search-input-${name}`, classes.root, dense && classes.dense, className)}
|
||||
classes={{ input: classes.input }}
|
||||
id={`search-input-${name}`}
|
||||
name={name}
|
||||
inputRef={input => this.input = input}
|
||||
value={this.state.value}
|
||||
type="search"
|
||||
onChange={this.updateSearch}
|
||||
onFocus={this.handleFocus}
|
||||
disableUnderline={true}
|
||||
startAdornment={searchIcon}
|
||||
endAdornment={clearButton}
|
||||
/>
|
||||
<NoSsr>
|
||||
{
|
||||
// KeyboardEventHandler is not valid on the server, where its name is undefined
|
||||
typeof window !== 'undefined' && KeyboardEventHandler.name && !noShortcuts &&
|
||||
|
||||
<KeyboardEventHandler handleKeys={['s', 'c', 'esc']} onKeyEvent={this.handleShortcutKeys}/>
|
||||
}
|
||||
</NoSsr>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
SearchInput.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
updateQuery: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
dense: PropTypes.bool,
|
||||
noShortcuts: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
SearchInput.defaultProps = {
|
||||
name: 'search',
|
||||
};
|
||||
|
||||
|
||||
SearchInput.displayName = 'SearchInput';
|
||||
|
||||
|
||||
registerComponent('SearchInput', SearchInput, [withStyles, styles]);
|
|
@ -0,0 +1,108 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import withTheme from '@material-ui/core/styles/withTheme';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {},
|
||||
tooltip: {
|
||||
margin: '4px !important',
|
||||
},
|
||||
buttonWrap: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
button: {},
|
||||
});
|
||||
|
||||
|
||||
const TooltipIconButton = (props, { intl }) => {
|
||||
|
||||
const {
|
||||
title,
|
||||
titleId,
|
||||
placement,
|
||||
icon,
|
||||
className,
|
||||
classes,
|
||||
theme,
|
||||
buttonRef,
|
||||
variant,
|
||||
...properties
|
||||
} = props;
|
||||
|
||||
const titleText = props.title || intl.formatMessage({ id: titleId });
|
||||
const slug = Utils.slugify(titleId);
|
||||
|
||||
return (
|
||||
<Tooltip classes={{ tooltip: classNames('tooltip-icon-button', classes.tooltip, className) }}
|
||||
id={`tooltip-${slug}`}
|
||||
title={titleText}
|
||||
placement={placement}
|
||||
enterDelay={theme.utils.tooltipEnterDelay}
|
||||
>
|
||||
<div className={classes.buttonWrap}>
|
||||
{
|
||||
variant === 'fab'
|
||||
|
||||
?
|
||||
|
||||
<Button className={classNames(classes.button, slug)}
|
||||
variant="fab"
|
||||
aria-label={title}
|
||||
ref={buttonRef}
|
||||
{...properties}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
|
||||
:
|
||||
|
||||
<IconButton className={classNames(classes.button, slug)}
|
||||
aria-label={title}
|
||||
ref={buttonRef}
|
||||
{...properties}
|
||||
>
|
||||
{icon}
|
||||
</IconButton>
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
TooltipIconButton.propTypes = {
|
||||
title: PropTypes.node,
|
||||
titleId: PropTypes.string,
|
||||
placement: PropTypes.string,
|
||||
icon: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
buttonRef: PropTypes.func,
|
||||
variant: PropTypes.string,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
TooltipIconButton.defaultProps = {
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
|
||||
TooltipIconButton.contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
|
||||
TooltipIconButton.displayName = 'TooltipIconButton';
|
||||
|
||||
|
||||
registerComponent('TooltipIconButton', TooltipIconButton, [withStyles, styles], [withTheme]);
|
168
packages/vulcan-ui-material/components/bonus/TooltipIntl.jsx
Normal file
168
packages/vulcan-ui-material/components/bonus/TooltipIntl.jsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent, Utils } from 'meteor/vulcan:core';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import withTheme from '@material-ui/core/styles/withTheme';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
|
||||
root: {
|
||||
display: 'inherit',
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
margin: '4px !important',
|
||||
},
|
||||
|
||||
buttonWrap: {
|
||||
display: 'inherit',
|
||||
},
|
||||
|
||||
button: {},
|
||||
|
||||
icon: {},
|
||||
|
||||
popoverPopper: {
|
||||
zIndex: 1700,
|
||||
},
|
||||
|
||||
popoverTooltip: {
|
||||
zIndex: 1701,
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const TooltipIntl = (props, { intl }) => {
|
||||
|
||||
const {
|
||||
title,
|
||||
titleId,
|
||||
titleValues,
|
||||
placement,
|
||||
icon,
|
||||
className,
|
||||
classes,
|
||||
theme,
|
||||
enterDelay,
|
||||
leaveDelay,
|
||||
buttonRef,
|
||||
variant,
|
||||
parent,
|
||||
children,
|
||||
...properties
|
||||
} = props;
|
||||
|
||||
const iconWithClass = icon && React.cloneElement(icon, { className: classes.icon });
|
||||
const popperClass = parent === 'popover' && classes.popoverPopper;
|
||||
const tooltipClass = parent === 'popover' && classes.popoverTooltip;
|
||||
const tooltipEnterDelay = typeof enterDelay === 'number' ? enterDelay : theme.utils.tooltipEnterDelay;
|
||||
const tooltipLeaveDelay = typeof leaveDelay === 'number' ? leaveDelay : theme.utils.tooltipLeaveDelay;
|
||||
const titleText = props.title || intl.formatMessage({ id: titleId }, titleValues);
|
||||
const slug = Utils.slugify(titleId);
|
||||
|
||||
return (
|
||||
<span className={classNames('tooltip-intl', classes.root, className)}>
|
||||
<Tooltip id={`tooltip-${slug}`}
|
||||
title={titleText}
|
||||
placement={placement}
|
||||
enterDelay={tooltipEnterDelay}
|
||||
leaveDelay={tooltipLeaveDelay}
|
||||
classes={{
|
||||
tooltip: classNames(classes.tooltip, tooltipClass),
|
||||
popper: popperClass,
|
||||
}}
|
||||
>
|
||||
<span className={classes.buttonWrap}>
|
||||
{
|
||||
variant === 'fab' && !!icon
|
||||
|
||||
?
|
||||
|
||||
<Button className={classNames(classes.button, slug)}
|
||||
variant="fab"
|
||||
aria-label={title}
|
||||
ref={buttonRef}
|
||||
{...properties}
|
||||
>
|
||||
{iconWithClass}
|
||||
</Button>
|
||||
|
||||
:
|
||||
|
||||
!!icon
|
||||
|
||||
?
|
||||
|
||||
<IconButton className={classNames(classes.button, slug)}
|
||||
aria-label={title}
|
||||
ref={buttonRef}
|
||||
{...properties}
|
||||
>
|
||||
{iconWithClass}
|
||||
</IconButton>
|
||||
|
||||
:
|
||||
|
||||
variant === 'button'
|
||||
|
||||
?
|
||||
<Button className={classNames(classes.button, slug)}
|
||||
aria-label={title}
|
||||
ref={buttonRef}
|
||||
{...properties}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
:
|
||||
|
||||
children
|
||||
}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
TooltipIntl.propTypes = {
|
||||
title: PropTypes.node,
|
||||
titleId: PropTypes.string,
|
||||
titleValues: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
buttonRef: PropTypes.func,
|
||||
variant: PropTypes.string,
|
||||
theme: PropTypes.object,
|
||||
enterDelay: PropTypes.number,
|
||||
leaveDelay: PropTypes.number,
|
||||
parent: PropTypes.oneOf(['default', 'popover']),
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
|
||||
TooltipIntl.defaultProps = {
|
||||
placement: 'bottom',
|
||||
parent: 'default',
|
||||
};
|
||||
|
||||
|
||||
TooltipIntl.contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
|
||||
TooltipIntl.displayName = 'TooltipIntl';
|
||||
|
||||
|
||||
registerComponent('TooltipIntl', TooltipIntl, [withStyles, styles], [withTheme]);
|
212
packages/vulcan-ui-material/components/core/Card.jsx
Normal file
212
packages/vulcan-ui-material/components/core/Card.jsx
Normal file
|
@ -0,0 +1,212 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import { replaceComponent, Components } from 'meteor/vulcan:core';
|
||||
import moment from 'moment';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import EditIcon from 'mdi-material-ui/Pencil';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const getLabel = (field, fieldName, collection, intl) => {
|
||||
const schema = collection.simpleSchema()._schema;
|
||||
const fieldSchema = schema[fieldName];
|
||||
if (fieldSchema) {
|
||||
return intl.formatMessage(
|
||||
{ id: `${collection._name}.${fieldName}`, defaultMessage: fieldSchema.label });
|
||||
} else {
|
||||
return fieldName;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getTypeName = (field, fieldName, collection) => {
|
||||
const schema = collection.simpleSchema()._schema;
|
||||
const fieldSchema = schema[fieldName];
|
||||
if (fieldSchema) {
|
||||
const type = fieldSchema.type.singleType;
|
||||
const typeName = typeof type === 'function' ? type.name : type;
|
||||
return typeName;
|
||||
} else {
|
||||
return typeof field;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const parseImageUrl = value => {
|
||||
const isImage = ['.png', '.jpg', '.gif'].indexOf(value.substr(-4)) !== -1 ||
|
||||
['.webp', '.jpeg'].indexOf(value.substr(-5)) !== -1;
|
||||
return isImage ?
|
||||
<img style={{ width: '100%', maxWidth: 200 }} src={value} alt={value}/> :
|
||||
<LimitedString string={value}/>;
|
||||
};
|
||||
|
||||
|
||||
const LimitedString = ({ string }) =>
|
||||
<div>
|
||||
{string.indexOf(' ') === -1 && string.length > 30 ?
|
||||
<span title={string}>{string.substr(0, 30)}…</span> :
|
||||
<span>{string}</span>
|
||||
}
|
||||
</div>;
|
||||
|
||||
|
||||
export const getFieldValue = (value, typeName, classes={}) => {
|
||||
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
typeName = 'Array';
|
||||
}
|
||||
|
||||
if (typeof typeName === 'undefined') {
|
||||
typeName = typeof value;
|
||||
}
|
||||
|
||||
switch (typeName) {
|
||||
|
||||
case 'Boolean':
|
||||
case 'boolean':
|
||||
return <Checkbox checked={value} disabled style={{ width: '32px', height: '32px' }}/>;
|
||||
|
||||
case 'Number':
|
||||
case 'number':
|
||||
case 'SimpleSchema.Integer':
|
||||
return <code>{value.toString()}</code>;
|
||||
|
||||
case 'Array':
|
||||
return <ol>{value.map(
|
||||
(item, index) => <li key={index}>{getFieldValue(item, typeof item, classes)}</li>)}</ol>;
|
||||
|
||||
case 'Object':
|
||||
case 'object':
|
||||
return (
|
||||
<Table className="table">
|
||||
<TableBody>
|
||||
{_.map(value, (value, key) =>
|
||||
<TableRow className={classNames(classes.table, 'table')} key={key}>
|
||||
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">{key}</TableCell>
|
||||
<TableCell className={classNames(classes.tableCell, 'datacard-value')} >{getFieldValue(value, typeof value, classes)}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
case 'Date':
|
||||
return moment(new Date(value)).format('dddd, MMMM Do YYYY, h:mm:ss');
|
||||
|
||||
default:
|
||||
return parseImageUrl(value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const CardItem = ({ label, value, typeName, classes }) =>
|
||||
<TableRow className={classes.tableRow}>
|
||||
<TableCell className={classNames(classes.tableHeadCell, 'datacard-label')} variant="head">
|
||||
{label}
|
||||
</TableCell>
|
||||
<TableCell className={classNames(classes.tableCell, 'datacard-value')}>
|
||||
{getFieldValue(value, typeName, classes)}
|
||||
</TableCell>
|
||||
</TableRow>;
|
||||
|
||||
|
||||
const CardEdit = (props, context) => {
|
||||
const classes = props.classes;
|
||||
const editTitle = context.intl.formatMessage({ id: 'cards.edit' });
|
||||
return (
|
||||
<TableRow className={classes.tableRow}>
|
||||
<TableCell className={classes.tableCell} colSpan="2">
|
||||
<Components.ModalTrigger label={editTitle}
|
||||
component={<IconButton aria-label={editTitle}>
|
||||
<EditIcon/>
|
||||
</IconButton>}
|
||||
>
|
||||
<CardEditForm {...props} />
|
||||
</Components.ModalTrigger>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
CardEdit.contextTypes = { intl: intlShape };
|
||||
|
||||
|
||||
const CardEditForm = ({ collection, document, closeModal }) =>
|
||||
<Components.SmartForm
|
||||
collection={collection}
|
||||
documentId={document._id}
|
||||
showRemove={true}
|
||||
successCallback={document => {
|
||||
closeModal();
|
||||
}}
|
||||
/>;
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {},
|
||||
table: {
|
||||
maxWidth: '100%'
|
||||
},
|
||||
tableBody: {},
|
||||
tableRow: {},
|
||||
tableCell: {},
|
||||
tableHeadCell: {},
|
||||
});
|
||||
|
||||
|
||||
const Card = ({ className, collection, document, currentUser, fields, classes }, { intl }) => {
|
||||
|
||||
const fieldNames = fields ? fields : _.without(_.keys(document), '__typename');
|
||||
const canUpdate = currentUser && collection.options.mutations.update.check(currentUser, document);
|
||||
|
||||
return (
|
||||
<div className={classNames(classes.root, 'datacard', `datacard-${collection._name}`, className)}>
|
||||
<Table className={classNames(classes.table, 'table')} style={{ maxWidth: '100%' }}>
|
||||
<TableBody>
|
||||
{canUpdate ? <CardEdit collection={collection} document={document} classes={classes}/> : null}
|
||||
{fieldNames.map((fieldName, index) =>
|
||||
<CardItem key={index}
|
||||
value={document[fieldName]}
|
||||
typeName={getTypeName(document[fieldName], fieldName, collection)}
|
||||
label={getLabel(document[fieldName], fieldName, collection, intl)}
|
||||
classes={classes}
|
||||
/>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
|
||||
Card.propTypes = {
|
||||
className: PropTypes.string,
|
||||
collection: PropTypes.object,
|
||||
document: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
fields: PropTypes.array,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
Card.contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('Card', Card, [withStyles, styles]);
|
644
packages/vulcan-ui-material/components/core/Datatable.jsx
Normal file
644
packages/vulcan-ui-material/components/core/Datatable.jsx
Normal file
|
@ -0,0 +1,644 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Components,
|
||||
registerComponent,
|
||||
replaceComponent,
|
||||
withCurrentUser,
|
||||
withMulti,
|
||||
Utils
|
||||
} from 'meteor/vulcan:core';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableFooter from '@material-ui/core/TableFooter';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import TableSortLabel from '@material-ui/core/TableSortLabel';
|
||||
import TablePagination from '@material-ui/core/TablePagination';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { getFieldValue } from './Card';
|
||||
import _assign from 'lodash/assign';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Datatable Component
|
||||
|
||||
*/
|
||||
const baseStyles = theme => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
addButton: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: 0,
|
||||
},
|
||||
search: {
|
||||
marginBottom: theme.spacing.unit * 8,
|
||||
},
|
||||
table: {},
|
||||
denseTable: {},
|
||||
denserTable: {},
|
||||
flatTable: {},
|
||||
tableHead: {},
|
||||
tableBody: {},
|
||||
tableFooter: {},
|
||||
tableRow: {},
|
||||
tableHeadCell: {},
|
||||
tableCell: {},
|
||||
clickRow: {},
|
||||
editCell: {},
|
||||
editButton: {}
|
||||
});
|
||||
|
||||
const delay = (function () {
|
||||
var timer = 0;
|
||||
return function (callback, ms) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(callback, ms);
|
||||
};
|
||||
})();
|
||||
|
||||
class Datatable extends PureComponent {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.updateQuery = this.updateQuery.bind(this);
|
||||
|
||||
this.state = {
|
||||
value: '',
|
||||
query: '',
|
||||
currentSort: {},
|
||||
};
|
||||
}
|
||||
|
||||
toggleSort = column => {
|
||||
let currentSort;
|
||||
if (!this.state.currentSort[column]) {
|
||||
currentSort = { [column]: 1 };
|
||||
} else if (this.state.currentSort[column] === 1) {
|
||||
currentSort = { [column]: -1 };
|
||||
} else {
|
||||
currentSort = {};
|
||||
}
|
||||
this.setState({ currentSort });
|
||||
};
|
||||
|
||||
updateQuery (value) {
|
||||
this.setState({
|
||||
value: value
|
||||
});
|
||||
delay(() => {
|
||||
this.setState({
|
||||
query: value
|
||||
});
|
||||
}, 700);
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.props.data) {
|
||||
|
||||
return <Components.DatatableContents
|
||||
columns={this.props.data.length ? Object.keys(this.props.data[0]) : undefined}
|
||||
{...this.props}
|
||||
results={this.props.data}
|
||||
count={this.props.data.length}
|
||||
totalCount={this.props.data.length}
|
||||
showEdit={false}
|
||||
showNew={false}
|
||||
/>;
|
||||
|
||||
} else {
|
||||
|
||||
const {
|
||||
className,
|
||||
collection,
|
||||
options,
|
||||
showSearch,
|
||||
showNew,
|
||||
currentUser,
|
||||
classes,
|
||||
} = this.props;
|
||||
|
||||
const listOptions = {
|
||||
collection: collection,
|
||||
...options,
|
||||
};
|
||||
|
||||
const DatatableWithMulti = withMulti(listOptions)(Components.DatatableContents);
|
||||
|
||||
// add _id to orderBy when we want to sort a column, to avoid breaking the graphql() hoc;
|
||||
// see https://github.com/VulcanJS/Vulcan/issues/2090#issuecomment-433860782
|
||||
// this.state.currentSort !== {} is always false, even when console.log(this.state.currentSort) displays
|
||||
// {}. So we test on the length of keys for this object.
|
||||
const orderBy = Object.keys(this.state.currentSort).length == 0 ? {} :
|
||||
{ ...this.state.currentSort, _id: -1 };
|
||||
|
||||
return (
|
||||
<div className={classNames('datatable', `datatable-${collection._name}`, classes.root,
|
||||
className)}>
|
||||
{/* DatatableAbove Component part*/}
|
||||
{
|
||||
showSearch &&
|
||||
|
||||
<Components.SearchInput value={this.state.query}
|
||||
updateQuery={this.updateQuery}
|
||||
className={classes.search}
|
||||
/>
|
||||
}
|
||||
{
|
||||
showNew &&
|
||||
|
||||
<Components.NewButton collection={collection}
|
||||
variant="fab"
|
||||
color="primary"
|
||||
className={classes.addButton}
|
||||
/>
|
||||
}
|
||||
|
||||
<DatatableWithMulti {...this.props}
|
||||
collection={collection}
|
||||
terms={{ query: this.state.query, orderBy: orderBy }}
|
||||
currentUser={this.props.currentUser}
|
||||
toggleSort={this.toggleSort}
|
||||
currentSort={this.state.currentSort}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Datatable.propTypes = {
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
collection: PropTypes.object,
|
||||
options: PropTypes.object,
|
||||
columns: PropTypes.array,
|
||||
showEdit: PropTypes.bool,
|
||||
editComponent: PropTypes.func,
|
||||
showNew: PropTypes.bool,
|
||||
showSearch: PropTypes.bool,
|
||||
emptyState: PropTypes.node,
|
||||
currentUser: PropTypes.object,
|
||||
classes: PropTypes.object,
|
||||
data: PropTypes.array,
|
||||
footerData: PropTypes.array,
|
||||
dense: PropTypes.string,
|
||||
queryDataRef: PropTypes.func,
|
||||
rowClass: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
handleRowClick: PropTypes.func,
|
||||
intlNamespace: PropTypes.string,
|
||||
toggleSort: PropTypes.func,
|
||||
currentSort: PropTypes.object,
|
||||
paginate: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
Datatable.defaultProps = {
|
||||
showNew: true,
|
||||
showEdit: true,
|
||||
showSearch: true,
|
||||
paginate: false,
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('Datatable', Datatable, withCurrentUser, [withStyles, baseStyles]);
|
||||
|
||||
|
||||
/*
|
||||
|
||||
DatatableContents Component
|
||||
|
||||
*/
|
||||
const datatableContentsStyles = theme => (_assign({}, baseStyles(theme), {
|
||||
table: {
|
||||
marginTop: theme.spacing.unit * 3,
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
},
|
||||
denseTable: theme.utils.denseTable,
|
||||
flatTable: theme.utils.flatTable,
|
||||
denserTable: theme.utils.denserTable,
|
||||
}));
|
||||
|
||||
|
||||
const DatatableContents = ({
|
||||
collection,
|
||||
columns,
|
||||
results,
|
||||
loading,
|
||||
loadMore,
|
||||
count,
|
||||
totalCount,
|
||||
networkStatus,
|
||||
refetch,
|
||||
showEdit,
|
||||
editComponent,
|
||||
emptyState,
|
||||
currentUser,
|
||||
classes,
|
||||
footerData,
|
||||
dense,
|
||||
queryDataRef,
|
||||
rowClass,
|
||||
handleRowClick,
|
||||
intlNamespace,
|
||||
title,
|
||||
toggleSort,
|
||||
currentSort,
|
||||
paginate,
|
||||
paginationTerms,
|
||||
setPaginationTerms
|
||||
}) => {
|
||||
|
||||
if (loading) {
|
||||
return <Components.Loading/>;
|
||||
} else if (!results || !results.length) {
|
||||
return emptyState || null;
|
||||
}
|
||||
|
||||
if (queryDataRef) queryDataRef(this.props);
|
||||
|
||||
const denseClass = dense && classes[dense + 'Table'];
|
||||
|
||||
// Pagination functions
|
||||
const getPage = (paginationTerms) => (parseInt((paginationTerms.limit - 1) / paginationTerms.itemsPerPage));
|
||||
|
||||
const onChangePage = (event, page) => {
|
||||
setPaginationTerms({
|
||||
itemsPerPage: paginationTerms.itemsPerPage,
|
||||
limit: (page + 1) * paginationTerms.itemsPerPage,
|
||||
offset: page * paginationTerms.itemsPerPage
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeRowsPerPage = (event) => {
|
||||
let value = event.target.value;
|
||||
let offset = Math.max(0, parseInt((paginationTerms.limit - paginationTerms.itemsPerPage) / value) * value);
|
||||
let limit = Math.min(offset + value, totalCount);
|
||||
setPaginationTerms({
|
||||
itemsPerPage: value,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
title &&
|
||||
<Toolbar>
|
||||
<Typography variant="h6" id="tableTitle">
|
||||
title
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
}
|
||||
<Table className={classNames(classes.table, denseClass)}>
|
||||
<TableHead className={classes.tableHead}>
|
||||
<TableRow className={classes.tableRow}>
|
||||
{
|
||||
_.sortBy(columns, column => column.order).map(
|
||||
(column, index) =>
|
||||
<Components.DatatableHeader key={index}
|
||||
collection={collection}
|
||||
intlNamespace={intlNamespace}
|
||||
column={column}
|
||||
classes={classes}
|
||||
toggleSort={toggleSort}
|
||||
currentSort={currentSort}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
(showEdit || editComponent) &&
|
||||
|
||||
<TableCell className={classes.tableCell}/>
|
||||
}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
{
|
||||
results &&
|
||||
|
||||
<TableBody className={classes.tableBody}>
|
||||
{
|
||||
results.map(
|
||||
(document, index) =>
|
||||
<Components.DatatableRow collection={collection}
|
||||
columns={columns}
|
||||
document={document}
|
||||
refetch={refetch}
|
||||
key={index}
|
||||
showEdit={showEdit}
|
||||
editComponent={editComponent}
|
||||
currentUser={currentUser}
|
||||
classes={classes}
|
||||
rowClass={rowClass}
|
||||
handleRowClick={handleRowClick}
|
||||
/>)
|
||||
}
|
||||
</TableBody>
|
||||
}
|
||||
|
||||
{
|
||||
footerData &&
|
||||
|
||||
<TableFooter className={classes.tableFooter}>
|
||||
<TableRow className={classes.tableRow}>
|
||||
{
|
||||
_.sortBy(columns, column => column.order).map(
|
||||
(column, index) =>
|
||||
<TableCell key={index} className={classNames(classes.tableCell, column.footerClass)}>
|
||||
{footerData[index]}
|
||||
</TableCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
(showEdit || editComponent) &&
|
||||
|
||||
<TableCell className={classes.tableCell}/>
|
||||
}
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
|
||||
}
|
||||
|
||||
</Table>
|
||||
{
|
||||
paginate &&
|
||||
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={totalCount}
|
||||
rowsPerPage={paginationTerms.itemsPerPage}
|
||||
page={getPage(paginationTerms)}
|
||||
backIconButtonProps={{
|
||||
'aria-label': 'Previous Page',
|
||||
}}
|
||||
nextIconButtonProps={{
|
||||
'aria-label': 'Next Page',
|
||||
}}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangeRowsPerPage}
|
||||
/>
|
||||
}
|
||||
{
|
||||
!paginate && loadMore &&
|
||||
|
||||
<Components.LoadMore className={classes.loadMore}
|
||||
count={count}
|
||||
totalCount={totalCount}
|
||||
loadMore={loadMore}
|
||||
networkStatus={networkStatus}
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('DatatableContents', DatatableContents, [withStyles, datatableContentsStyles]);
|
||||
|
||||
|
||||
/*
|
||||
|
||||
DatatableHeader Component
|
||||
|
||||
*/
|
||||
const DatatableHeader = ({ collection, intlNamespace, column, classes, toggleSort, currentSort }, { intl }) => {
|
||||
const columnName = typeof column === 'string' ? column : column.name || column.label;
|
||||
let formattedLabel = '';
|
||||
|
||||
if (collection) {
|
||||
const schema = collection.simpleSchema()._schema;
|
||||
|
||||
/*
|
||||
use either:
|
||||
|
||||
1. the column name translation
|
||||
2. the column name label in the schema (if the column name matches a schema field)
|
||||
3. the raw column name.
|
||||
*/
|
||||
const defaultMessage = schema[columnName] ? schema[columnName].label : Utils.camelToSpaces(columnName);
|
||||
formattedLabel = typeof columnName === 'string' ?
|
||||
intl.formatMessage({
|
||||
id: `${collection._name}.${columnName}`,
|
||||
defaultMessage: defaultMessage
|
||||
}) :
|
||||
'';
|
||||
|
||||
// if sortable is a string, use it as the name of the property to sort by. If it's just `true`, use
|
||||
// column.name
|
||||
const sortPropertyName = typeof column.sortable === 'string' ? column.sortable : column.name;
|
||||
|
||||
if (column.sortable) {
|
||||
return <Components.DatatableSorter name={sortPropertyName}
|
||||
label={formattedLabel}
|
||||
toggleSort={toggleSort}
|
||||
currentSort={currentSort}
|
||||
sortable={column.sortable}
|
||||
/>;
|
||||
}
|
||||
} else if (intlNamespace) {
|
||||
formattedLabel = typeof columnName === 'string' ?
|
||||
intl.formatMessage({
|
||||
id: `${intlNamespace}.${columnName}`,
|
||||
defaultMessage: columnName
|
||||
}) :
|
||||
'';
|
||||
} else {
|
||||
formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName });
|
||||
}
|
||||
|
||||
return <TableCell
|
||||
className={classNames(classes.tableHeadCell, column.headerClass)}>{formattedLabel}</TableCell>;
|
||||
};
|
||||
|
||||
|
||||
DatatableHeader.contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('DatatableHeader', DatatableHeader);
|
||||
|
||||
|
||||
/*
|
||||
|
||||
DatatableSorter Component
|
||||
|
||||
*/
|
||||
|
||||
const DatatableSorter = ({ name, label, toggleSort, currentSort, sortable }) =>
|
||||
<TableCell className="datatable-sorter"
|
||||
sortDirection={!currentSort[name] ? false : currentSort[name] === 1 ? 'asc' : 'desc'}
|
||||
>
|
||||
<Tooltip
|
||||
title="Sort"
|
||||
placement='bottom-start'
|
||||
enterDelay={300}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={!currentSort[name] ? false : true}
|
||||
direction={currentSort[name] === 1 ? 'desc' : 'asc'}
|
||||
onClick={() => toggleSort(name)}
|
||||
>
|
||||
{label}
|
||||
</TableSortLabel>
|
||||
</Tooltip>
|
||||
</TableCell>;
|
||||
|
||||
replaceComponent('DatatableSorter', DatatableSorter);
|
||||
|
||||
|
||||
/*
|
||||
|
||||
DatatableRow Component
|
||||
|
||||
*/
|
||||
const datatableRowStyles = theme => (_assign({}, baseStyles(theme), {
|
||||
clickRow: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
editCell: {
|
||||
paddingTop: '0 !important',
|
||||
paddingBottom: '0 !important',
|
||||
textAlign: 'right',
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const DatatableRow = ({
|
||||
collection,
|
||||
columns,
|
||||
document,
|
||||
refetch,
|
||||
showEdit,
|
||||
editComponent,
|
||||
currentUser,
|
||||
rowClass,
|
||||
handleRowClick,
|
||||
classes,
|
||||
}, { intl }) => {
|
||||
|
||||
const EditComponent = editComponent;
|
||||
|
||||
if (typeof rowClass === 'function') {
|
||||
rowClass = rowClass(document);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className={classNames('datatable-item', classes.tableRow, rowClass, handleRowClick && classes.clickRow)}
|
||||
onClick={handleRowClick && (event => handleRowClick(event, document))}
|
||||
hover
|
||||
>
|
||||
|
||||
{
|
||||
_.sortBy(columns, column => column.order).map(
|
||||
(column, index) =>
|
||||
<Components.DatatableCell key={index}
|
||||
column={column}
|
||||
document={document}
|
||||
currentUser={currentUser}
|
||||
classes={classes}
|
||||
/>)
|
||||
}
|
||||
|
||||
{
|
||||
(showEdit || editComponent) &&
|
||||
|
||||
<TableCell className={classes.editCell}>
|
||||
{
|
||||
EditComponent &&
|
||||
|
||||
<EditComponent collection={collection} document={document} refetch={refetch}/>
|
||||
}
|
||||
{
|
||||
showEdit &&
|
||||
|
||||
<Components.EditButton collection={collection}
|
||||
document={document}
|
||||
buttonClasses={{ button: classes.editButton }}
|
||||
/>
|
||||
}
|
||||
</TableCell>
|
||||
}
|
||||
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('DatatableRow', DatatableRow, [withStyles, datatableRowStyles]);
|
||||
|
||||
|
||||
DatatableRow.contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
|
||||
DatatableCell Component
|
||||
|
||||
*/
|
||||
const DatatableCell = ({ column, document, currentUser, classes }) => {
|
||||
const Component = column.component ||
|
||||
Components[column.componentName] ||
|
||||
Components.DatatableDefaultCell;
|
||||
|
||||
const columnName = typeof column === 'string' ? column : column.name;
|
||||
const className = typeof columnName === 'string' ?
|
||||
`datatable-item-${columnName.toLowerCase()}` :
|
||||
'';
|
||||
const cellClass = typeof column.cellClass === 'function' ?
|
||||
column.cellClass({ column, document, currentUser }) :
|
||||
typeof column.cellClass === 'string' ?
|
||||
column.cellClass :
|
||||
null;
|
||||
|
||||
return (
|
||||
<TableCell className={classNames(classes.tableCell, cellClass, className)}>
|
||||
<Component column={column}
|
||||
document={document}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('DatatableCell', DatatableCell);
|
||||
|
||||
|
||||
/*
|
||||
|
||||
DatatableDefaultCell Component
|
||||
|
||||
*/
|
||||
const DatatableDefaultCell = ({ column, document }) =>
|
||||
<div>
|
||||
{
|
||||
typeof column === 'string'
|
||||
?
|
||||
getFieldValue(document[column])
|
||||
:
|
||||
getFieldValue(document[column.name])
|
||||
}
|
||||
</div>;
|
||||
|
||||
|
||||
replaceComponent('DatatableDefaultCell', DatatableDefaultCell);
|
86
packages/vulcan-ui-material/components/core/EditButton.jsx
Normal file
86
packages/vulcan-ui-material/components/core/EditButton.jsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import EditIcon from 'mdi-material-ui/Pencil';
|
||||
|
||||
|
||||
const EditButton = ({
|
||||
collection,
|
||||
document,
|
||||
color = 'default',
|
||||
variant,
|
||||
triggerClasses,
|
||||
buttonClasses,
|
||||
...props
|
||||
}, { intl }) => (
|
||||
|
||||
<Components.ModalTrigger
|
||||
classes={triggerClasses}
|
||||
component={<Components.TooltipIconButton titleId="datatable.edit"
|
||||
icon={<EditIcon/>}
|
||||
color={color}
|
||||
variant={variant}
|
||||
classes={buttonClasses}
|
||||
/>}
|
||||
>
|
||||
<Components.EditForm collection={collection} document={document} {...props}/>
|
||||
</Components.ModalTrigger>
|
||||
);
|
||||
|
||||
|
||||
EditButton.propTypes = {
|
||||
collection: PropTypes.object.isRequired,
|
||||
document: PropTypes.object.isRequired,
|
||||
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
|
||||
variant: PropTypes.string,
|
||||
triggerClasses: PropTypes.object,
|
||||
buttonClasses: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
EditButton.contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
|
||||
EditButton.displayName = 'EditButton';
|
||||
|
||||
|
||||
registerComponent('EditButton', EditButton);
|
||||
|
||||
|
||||
/*
|
||||
|
||||
EditForm Component
|
||||
|
||||
*/
|
||||
const EditForm = ({ collection, document, closeModal, options, successCallback, removeSuccessCallback,...props }) => {
|
||||
|
||||
const success = successCallback
|
||||
? () => {
|
||||
successCallback();
|
||||
closeModal();
|
||||
}
|
||||
: closeModal;
|
||||
|
||||
const remove = removeSuccessCallback
|
||||
? () => {
|
||||
removeSuccessCallback();
|
||||
closeModal();
|
||||
}
|
||||
: closeModal;
|
||||
|
||||
return (
|
||||
<Components.SmartForm
|
||||
{...props}
|
||||
collection={collection}
|
||||
documentId={document && document._id}
|
||||
showRemove={true}
|
||||
successCallback={success}
|
||||
removeSuccessCallback={remove}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
registerComponent('EditForm', EditForm);
|
9
packages/vulcan-ui-material/components/core/Loading.jsx
Normal file
9
packages/vulcan-ui-material/components/core/Loading.jsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { replaceComponent } from 'meteor/vulcan:core';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
|
||||
function Loading(props) {
|
||||
return <CircularProgress {...props} />;
|
||||
}
|
||||
|
||||
replaceComponent('Loading', Loading);
|
150
packages/vulcan-ui-material/components/core/ModalTrigger.jsx
Normal file
150
packages/vulcan-ui-material/components/core/ModalTrigger.jsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
button: {},
|
||||
anchor: {},
|
||||
dialog: {},
|
||||
dialogPaper: {},
|
||||
dialogTitle: {},
|
||||
dialogContent: {
|
||||
paddingTop: '4px',
|
||||
},
|
||||
dialogOverflow: {
|
||||
overflowY: 'visible',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
class ModalTrigger extends PureComponent {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = { modalIsOpen: false };
|
||||
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.action) {
|
||||
this.props.action({
|
||||
openModal: this.openModal,
|
||||
closeModal: this.closeModal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openModal = () => {
|
||||
this.setState({ modalIsOpen: true });
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({ modalIsOpen: false });
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
className,
|
||||
dialogClassName,
|
||||
dialogOverflow,
|
||||
labelId,
|
||||
component,
|
||||
titleId,
|
||||
type,
|
||||
children,
|
||||
classes,
|
||||
} = this.props;
|
||||
|
||||
const intl = this.context.intl;
|
||||
|
||||
const label = labelId ? intl.formatMessage({ id: labelId }) : this.props.label;
|
||||
const title = titleId ? intl.formatMessage({ id: titleId }) : this.props.title;
|
||||
const overflowClass = dialogOverflow && classes.dialogOverflow;
|
||||
|
||||
const triggerComponent = component
|
||||
?
|
||||
React.cloneElement(component, { onClick: this.openModal })
|
||||
:
|
||||
type === 'button'
|
||||
?
|
||||
<Button className={classes.button} variant="contained" onClick={this.openModal}>{label}</Button>
|
||||
:
|
||||
<a className={classes.anchor} href="#" onClick={this.openModal}>{label}</a>;
|
||||
|
||||
const childrenComponent = typeof children.type === 'function' ?
|
||||
React.cloneElement(children, { closeModal: this.closeModal }) :
|
||||
children;
|
||||
|
||||
return (
|
||||
<span className={classNames('modal-trigger', classes.root, className)}>
|
||||
|
||||
{triggerComponent}
|
||||
|
||||
<Dialog className={classNames(dialogClassName)}
|
||||
open={this.state.modalIsOpen}
|
||||
onClose={this.closeModal}
|
||||
fullWidth={true}
|
||||
classes={{ paper: classNames(classes.dialogPaper, overflowClass) }}
|
||||
>
|
||||
|
||||
{
|
||||
title &&
|
||||
|
||||
<DialogTitle className={classes.dialogTitle}>{title}</DialogTitle>
|
||||
}
|
||||
|
||||
<DialogContent className={classNames(classes.dialogContent, overflowClass)}>
|
||||
{childrenComponent}
|
||||
</DialogContent>
|
||||
|
||||
</Dialog>
|
||||
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ModalTrigger.propTypes = {
|
||||
/**
|
||||
* Callback fired when the component mounts.
|
||||
* This is useful when you want to trigger an action programmatically.
|
||||
* It supports `openModal()` and `closeModal()`.
|
||||
*
|
||||
* @param {object} actions This object contains all possible actions
|
||||
* that can be triggered programmatically.
|
||||
*/
|
||||
action: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
dialogClassName: PropTypes.string,
|
||||
dialogOverflow: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
labelId: PropTypes.string,
|
||||
component: PropTypes.object,
|
||||
title: PropTypes.node,
|
||||
titleId: PropTypes.string,
|
||||
type: PropTypes.oneOf(['link', 'button']),
|
||||
children: PropTypes.node,
|
||||
classes: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
ModalTrigger.contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
|
||||
registerComponent('ModalTrigger', ModalTrigger, [withStyles, styles]);
|
44
packages/vulcan-ui-material/components/core/NewButton.jsx
Normal file
44
packages/vulcan-ui-material/components/core/NewButton.jsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import AddIcon from 'mdi-material-ui/Plus';
|
||||
|
||||
|
||||
const NewButton = ({
|
||||
className,
|
||||
collection,
|
||||
color = 'default',
|
||||
variant,
|
||||
}, { intl }) => (
|
||||
|
||||
<Components.ModalTrigger
|
||||
className={className}
|
||||
component={<Components.TooltipIconButton titleId="datatable.new"
|
||||
icon={<AddIcon/>}
|
||||
color={color}
|
||||
variant={variant}
|
||||
/>}
|
||||
>
|
||||
<Components.EditForm collection={collection}/>
|
||||
</Components.ModalTrigger>
|
||||
);
|
||||
|
||||
|
||||
NewButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
collection: PropTypes.object.isRequired,
|
||||
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
|
||||
variant: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
NewButton.contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
|
||||
NewButton.displayName = 'NewButton';
|
||||
|
||||
|
||||
replaceComponent('NewButton', NewButton);
|
|
@ -0,0 +1,129 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import _omit from 'lodash/omit';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
|
||||
formInput: {
|
||||
position: 'relative',
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
},
|
||||
|
||||
halfWidthLeft: {
|
||||
display: 'inline-block',
|
||||
width: '48%',
|
||||
verticalAlign: 'top',
|
||||
marginRight: '4%',
|
||||
},
|
||||
|
||||
halfWidthRight: {
|
||||
display: 'inline-block',
|
||||
width: '48%',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
|
||||
thirdWidthLeft: {
|
||||
display: 'inline-block',
|
||||
width: '31%',
|
||||
verticalAlign: 'top',
|
||||
marginRight: '3.5%',
|
||||
},
|
||||
|
||||
thirdWidthRight: {
|
||||
display: 'inline-block',
|
||||
width: '31%',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
|
||||
hidden: {
|
||||
display: 'none',
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
class FormComponentInner extends PureComponent {
|
||||
|
||||
getProperties = () => {
|
||||
return _omit(this.props, 'classes');
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
classes,
|
||||
inputClassName,
|
||||
name,
|
||||
input,
|
||||
hidden,
|
||||
beforeComponent,
|
||||
afterComponent,
|
||||
formInput,
|
||||
intlInput,
|
||||
nestedInput,
|
||||
formComponents,
|
||||
} = this.props;
|
||||
|
||||
const FormComponents = formComponents;
|
||||
|
||||
const inputClass = classNames(
|
||||
classes.formInput,
|
||||
hidden && classes.hidden,
|
||||
inputClassName && classes[inputClassName],
|
||||
`input-${name}`,
|
||||
`form-component-${input || 'default'}`
|
||||
);
|
||||
|
||||
const properties = this.getProperties();
|
||||
|
||||
const FormInput = formInput;
|
||||
|
||||
if (intlInput) {
|
||||
return <Components.FormIntl {...properties} />;
|
||||
} else if (nestedInput){
|
||||
return <Components.FormNested {...properties} />;
|
||||
} else {
|
||||
return (
|
||||
<div className={inputClass}>
|
||||
{instantiateComponent(beforeComponent, properties)}
|
||||
<FormInput {...properties}/>
|
||||
{instantiateComponent(afterComponent, properties)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FormComponentInner.contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
|
||||
FormComponentInner.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
inputClassName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
input: PropTypes.any,
|
||||
beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
errors: PropTypes.array.isRequired,
|
||||
help: PropTypes.node,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
showCharsRemaining: PropTypes.bool.isRequired,
|
||||
charsRemaining: PropTypes.number,
|
||||
charsCount: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
formInput: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
FormComponentInner.displayName = 'FormComponentInner';
|
||||
|
||||
|
||||
registerComponent('FormComponentInner', FormComponentInner, [withStyles, styles]);
|
51
packages/vulcan-ui-material/components/forms/FormErrors.jsx
Normal file
51
packages/vulcan-ui-material/components/forms/FormErrors.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { replaceComponent, Components } from 'meteor/vulcan:core';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
boxShadow: 'none',
|
||||
marginBottom: theme.spacing.unit * 2
|
||||
},
|
||||
list: {
|
||||
marginBottom: 0
|
||||
},
|
||||
error: { '& > div': { backgroundColor: theme.palette.error[500] } },
|
||||
danger: { '& > div': { backgroundColor: theme.palette.error[500] } },
|
||||
warning: { '& > div': { backgroundColor: theme.palette.error[500] } }
|
||||
});
|
||||
|
||||
|
||||
const FormErrors = ({ errors, classes }) => {
|
||||
const messageNode = (
|
||||
<ul className={classes.list}>
|
||||
{errors.map((error, index) => (
|
||||
<li key={index}>
|
||||
<Components.FormError error={error} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!!errors.length && (
|
||||
<Snackbar
|
||||
open={true}
|
||||
className={classNames('flash-message', classes.root , classes.danger)}
|
||||
message={messageNode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('FormErrors', FormErrors, [withStyles, styles]);
|
205
packages/vulcan-ui-material/components/forms/FormGroup.jsx
Normal file
205
packages/vulcan-ui-material/components/forms/FormGroup.jsx
Normal file
|
@ -0,0 +1,205 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, replaceComponent, instantiateComponent, } from 'meteor/vulcan:core';
|
||||
import Users from 'meteor/vulcan:users';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
|
||||
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
minWidth: '320px'
|
||||
},
|
||||
paper: {
|
||||
padding: theme.spacing.unit * 3
|
||||
},
|
||||
subtitle1: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: theme.spacing.unit / 2,
|
||||
marginTop: theme.spacing.unit * 5,
|
||||
marginBottom: theme.spacing.unit,
|
||||
color: theme.palette.primary[500],
|
||||
},
|
||||
collapsible: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
label: {},
|
||||
toggle: {
|
||||
'& svg': {
|
||||
width: 21,
|
||||
height: 21,
|
||||
display: 'block',
|
||||
}
|
||||
},
|
||||
container: {
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
marginLeft: -4,
|
||||
marginRight: -4,
|
||||
},
|
||||
entered: {
|
||||
overflow: 'visible',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
class FormGroup extends PureComponent {
|
||||
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.isAdmin = props.name === 'admin';
|
||||
|
||||
this.state = {
|
||||
collapsed: props.startCollapsed || this.isAdmin,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
toggle = () => {
|
||||
const collapsible = this.props.collapsible || this.isAdmin;
|
||||
if (!collapsible) return;
|
||||
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
renderHeading = () => {
|
||||
const { classes, label } = this.props;
|
||||
const collapsible = this.props.collapsible || this.isAdmin;
|
||||
|
||||
return (
|
||||
<Typography className={classNames(classes.subtitle1, collapsible && classes.collapsible)}
|
||||
variant="subtitle1"
|
||||
onClick={this.toggle}
|
||||
>
|
||||
|
||||
<div className={classes.label}>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
{
|
||||
collapsible &&
|
||||
|
||||
<div className={classes.toggle}>
|
||||
{
|
||||
this.state.collapsed
|
||||
?
|
||||
<ExpandMoreIcon/>
|
||||
:
|
||||
<ExpandLessIcon/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// if at least one of the fields in the group has an error, the group as a whole has an error
|
||||
hasErrors = () => _.some(this.props.fields, field => {
|
||||
return !!this.props.errors.filter(error => error.path === field.path).length;
|
||||
});
|
||||
|
||||
|
||||
render () {
|
||||
const {
|
||||
name,
|
||||
hidden,
|
||||
classes,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
if (this.isAdmin && !Users.isAdmin(currentUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//do not display if no fields, no startComponent and no endComponent
|
||||
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
|
||||
const collapseIn = !this.state.collapsed || this.hasErrors();
|
||||
|
||||
const FormComponents = this.props.formComponents;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
<a name={anchorName}/>
|
||||
|
||||
{
|
||||
name === 'default'
|
||||
?
|
||||
null
|
||||
:
|
||||
this.renderHeading()
|
||||
}
|
||||
|
||||
<Collapse classes={{ container: classes.container, entered: classes.entered }} in={collapseIn}>
|
||||
<Paper className={classes.paper}>
|
||||
|
||||
{instantiateComponent(this.props.startComponent)}
|
||||
|
||||
{this.props.fields.map(field => (
|
||||
<FormComponents.FormComponent
|
||||
key={field.name}
|
||||
disabled={this.props.disabled}
|
||||
{...field}
|
||||
errors={this.props.errors}
|
||||
throwError={this.props.throwError}
|
||||
currentValues={this.props.currentValues}
|
||||
updateCurrentValues={this.props.updateCurrentValues}
|
||||
deletedValues={this.props.deletedValues}
|
||||
addToDeletedValues={this.props.addToDeletedValues}
|
||||
clearFieldErrors={this.props.clearFieldErrors}
|
||||
formType={this.props.formType}
|
||||
currentUser={this.props.currentUser}
|
||||
formComponents={FormComponents}
|
||||
/>
|
||||
))}
|
||||
|
||||
{instantiateComponent(this.props.endComponent)}
|
||||
|
||||
</Paper>
|
||||
</Collapse>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
FormGroup.propTypes = {
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
order: PropTypes.number,
|
||||
hidden: PropTypes.bool,
|
||||
fields: PropTypes.array,
|
||||
collapsible: PropTypes.bool,
|
||||
startCollapsed: PropTypes.bool,
|
||||
updateCurrentValues: PropTypes.func,
|
||||
startComponent: PropTypes.node,
|
||||
endComponent: PropTypes.node,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('FormGroup', FormGroup, [withStyles, styles]);
|
|
@ -0,0 +1,95 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Components,
|
||||
registerComponent,
|
||||
instantiateComponent,
|
||||
} from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Users from 'meteor/vulcan:users';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
minWidth: '320px'
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
class FormGroupNone extends PureComponent {
|
||||
|
||||
|
||||
render () {
|
||||
const {
|
||||
name,
|
||||
hidden,
|
||||
classes,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
if (this.isAdmin && !Users.isAdmin(currentUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//do not display if no fields, no startComponent and no endComponent
|
||||
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
|
||||
|
||||
const FormComponents = this.props.formComponents;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
<a name={anchorName}/>
|
||||
|
||||
{instantiateComponent(this.props.startComponent)}
|
||||
|
||||
{this.props.fields.map(field => (
|
||||
<FormComponents.FormComponent
|
||||
key={field.name}
|
||||
disabled={this.props.disabled}
|
||||
{...field}
|
||||
errors={this.props.errors}
|
||||
throwError={this.props.throwError}
|
||||
currentValues={this.props.currentValues}
|
||||
updateCurrentValues={this.props.updateCurrentValues}
|
||||
deletedValues={this.props.deletedValues}
|
||||
addToDeletedValues={this.props.addToDeletedValues}
|
||||
clearFieldErrors={this.props.clearFieldErrors}
|
||||
formType={this.props.formType}
|
||||
currentUser={this.props.currentUser}
|
||||
formComponents={FormComponents}
|
||||
/>
|
||||
))}
|
||||
|
||||
{instantiateComponent(this.props.endComponent)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
FormGroupNone.propTypes = {
|
||||
name: PropTypes.string,
|
||||
order: PropTypes.number,
|
||||
hidden: PropTypes.bool,
|
||||
fields: PropTypes.array,
|
||||
updateCurrentValues: PropTypes.func,
|
||||
startComponent: PropTypes.node,
|
||||
endComponent: PropTypes.node,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
registerComponent('FormGroupNone', FormGroupNone, [withStyles, styles]);
|
|
@ -0,0 +1,198 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
|
||||
import Users from 'meteor/vulcan:users';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
|
||||
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
minWidth: '320px',
|
||||
},
|
||||
divider: {
|
||||
marginLeft: theme.spacing.unit * -3,
|
||||
marginRight: theme.spacing.unit * -3,
|
||||
},
|
||||
subtitle1: {
|
||||
marginTop: theme.spacing.unit * 5,
|
||||
position: 'relative',
|
||||
},
|
||||
collapsible: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
typography: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'& > div': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
'& > div:first-child': {
|
||||
...theme.typography.subtitle1,
|
||||
},
|
||||
paddingTop: theme.spacing.unit,
|
||||
paddingBottom: theme.spacing.unit,
|
||||
},
|
||||
toggle: {
|
||||
color: theme.palette.action.active,
|
||||
},
|
||||
entered: {
|
||||
overflow: 'visible',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
class FormGroupWithLine extends PureComponent {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.isAdmin = props.name === 'admin';
|
||||
|
||||
this.state = {
|
||||
collapsed: props.startCollapsed || this.isAdmin,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
toggle = () => {
|
||||
const collapsible = this.props.collapsible || this.isAdmin;
|
||||
if (!collapsible) return;
|
||||
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
renderHeading = () => {
|
||||
const { classes } = this.props;
|
||||
const collapsible = this.props.collapsible || this.isAdmin;
|
||||
|
||||
return (
|
||||
<div className={classNames(classes.subtitle1, collapsible && classes.collapsible)} onClick={this.toggle}>
|
||||
|
||||
<Divider className={classes.divider}/>
|
||||
|
||||
<Typography className={classes.typography} variant="subtitle1" gutterBottom>
|
||||
<div>
|
||||
{this.props.label}
|
||||
</div>
|
||||
{
|
||||
collapsible &&
|
||||
|
||||
<div className={classes.toggle}>
|
||||
{
|
||||
this.state.collapsed
|
||||
?
|
||||
<ExpandMoreIcon/>
|
||||
:
|
||||
<ExpandLessIcon/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Typography>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// if at least one of the fields in the group has an error, the group as a whole has an error
|
||||
hasErrors = () => _.some(this.props.fields, field => {
|
||||
return !!this.props.errors.filter(error => error.path === field.path).length;
|
||||
});
|
||||
|
||||
|
||||
render () {
|
||||
const {
|
||||
name,
|
||||
hidden,
|
||||
classes,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
if (this.isAdmin && !Users.isAdmin(currentUser)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof hidden === 'function' ? hidden({ ...this.props }) : hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//do not display if no fields, no startComponent and no endComponent
|
||||
if (!this.props.startComponent && !this.props.endComponent && !this.props.fields.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const anchorName = name.split('.').length > 1 ? name.split('.')[1] : name;
|
||||
const collapseIn = !this.state.collapsed || this.hasErrors();
|
||||
|
||||
const FormComponents = this.props.formComponents;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
<a name={anchorName}/>
|
||||
|
||||
{
|
||||
this.props.name === 'default'
|
||||
?
|
||||
null
|
||||
:
|
||||
this.renderHeading()
|
||||
}
|
||||
|
||||
<Collapse classes={{ entered: classes.entered }} in={collapseIn}>
|
||||
|
||||
{instantiateComponent(this.props.startComponent)}
|
||||
|
||||
{this.props.fields.map(field => (
|
||||
<FormComponents.FormComponent
|
||||
key={field.name}
|
||||
disabled={this.props.disabled}
|
||||
{...field}
|
||||
errors={this.props.errors}
|
||||
throwError={this.props.throwError}
|
||||
currentValues={this.props.currentValues}
|
||||
updateCurrentValues={this.props.updateCurrentValues}
|
||||
deletedValues={this.props.deletedValues}
|
||||
addToDeletedValues={this.props.addToDeletedValues}
|
||||
clearFieldErrors={this.props.clearFieldErrors}
|
||||
formType={this.props.formType}
|
||||
currentUser={this.props.currentUser}
|
||||
formComponents={FormComponents}
|
||||
/>
|
||||
))}
|
||||
|
||||
{instantiateComponent(this.props.endComponent)}
|
||||
|
||||
</Collapse>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FormGroupWithLine.propTypes = {
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
order: PropTypes.number,
|
||||
fields: PropTypes.array,
|
||||
collapsible: PropTypes.bool,
|
||||
startCollapsed: PropTypes.bool,
|
||||
updateCurrentValues: PropTypes.func,
|
||||
startComponent: PropTypes.node,
|
||||
endComponent: PropTypes.node,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
registerComponent('FormGroupWithLine', FormGroupWithLine, [withStyles, styles]);
|
13
packages/vulcan-ui-material/components/forms/FormNested.jsx
Normal file
13
packages/vulcan-ui-material/components/forms/FormNested.jsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import { replaceComponent } from 'meteor/vulcan:core';
|
||||
import Delete from 'mdi-material-ui/Delete';
|
||||
import Plus from 'mdi-material-ui/Plus';
|
||||
|
||||
|
||||
const IconRemove = () => <Delete/>;
|
||||
|
||||
replaceComponent('IconRemove', IconRemove);
|
||||
|
||||
const IconAdd = () => <Plus/>;
|
||||
|
||||
replaceComponent('IconAdd', IconAdd);
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
|
||||
divider: {
|
||||
marginLeft: -24,
|
||||
marginRight: -24,
|
||||
marginTop: 16,
|
||||
marginBottom: 23,
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const FormNestedDivider = ({ classes, label, addItem }) => <Divider className={classes.divider}/>;
|
||||
|
||||
FormNestedDivider.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
label: PropTypes.string,
|
||||
addItem: PropTypes.func,
|
||||
};
|
||||
|
||||
registerComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]);
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
|
||||
const FormNestedFoot = ({ label, addItem }) => (
|
||||
<Grid container spacin={0} justify="flex-end">
|
||||
<Components.Button color="primary" variant="fab" mini onClick={addItem} className="form-nested-add">
|
||||
<Components.IconAdd/>
|
||||
</Components.Button>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
FormNestedFoot.propTypes = {
|
||||
label: PropTypes.string,
|
||||
addItem: PropTypes.func,
|
||||
};
|
||||
|
||||
registerComponent('FormNestedFoot', FormNestedFoot);
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { replaceComponent } from 'meteor/vulcan:core';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
|
||||
const FormNestedHead = ({ label, addItem }) => <span/>;
|
||||
|
||||
FormNestedHead.propTypes = {
|
||||
label: PropTypes.string,
|
||||
addItem: PropTypes.func,
|
||||
};
|
||||
|
||||
replaceComponent('FormNestedHead', FormNestedHead);
|
136
packages/vulcan-ui-material/components/forms/FormSubmit.jsx
Normal file
136
packages/vulcan-ui-material/components/forms/FormSubmit.jsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, replaceComponent } from 'meteor/vulcan:core';
|
||||
import { intlShape } from 'meteor/vulcan:i18n';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import DeleteIcon from 'mdi-material-ui/Delete';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
textAlign: 'center',
|
||||
marginTop: theme.spacing.unit * 4,
|
||||
},
|
||||
button: {
|
||||
margin: theme.spacing.unit,
|
||||
},
|
||||
delete: {
|
||||
float: 'left',
|
||||
},
|
||||
tooltip: {
|
||||
margin: 3,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const FormSubmit = ({
|
||||
submitLabel,
|
||||
cancelLabel,
|
||||
cancelCallback,
|
||||
revertLabel,
|
||||
revertCallback,
|
||||
document,
|
||||
deleteDocument,
|
||||
collectionName,
|
||||
classes
|
||||
}, {
|
||||
intl,
|
||||
isChanged,
|
||||
clearForm,
|
||||
}) => {
|
||||
|
||||
if (typeof isChanged !== 'function') {
|
||||
isChanged = () => true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
{
|
||||
deleteDocument
|
||||
?
|
||||
<Tooltip id={`tooltip-delete-${collectionName}`}
|
||||
classes={{ tooltip: classNames('delete-button', classes.tooltip) }}
|
||||
title={intl.formatMessage({ id: 'forms.delete' })}
|
||||
placement="bottom">
|
||||
<IconButton onClick={deleteDocument} className={classes.delete}>
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
cancelCallback
|
||||
?
|
||||
<Button variant="contained"
|
||||
className={classNames('cancel-button', classes.button)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
cancelCallback(document);
|
||||
}}>
|
||||
{cancelLabel ? cancelLabel : <FormattedMessage id="forms.cancel"/>}
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
revertCallback
|
||||
?
|
||||
<Button variant="contained"
|
||||
className={classNames('revert-button', classes.button)}
|
||||
disabled={!isChanged()}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
clearForm({ clearErrors: true, clearCurrentValues: true, clearDeletedValues: true });
|
||||
revertCallback(document);
|
||||
}}
|
||||
>
|
||||
{revertLabel ? revertLabel : <FormattedMessage id="forms.revert"/>}
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
||||
<Button variant="contained"
|
||||
type="submit"
|
||||
color="secondary"
|
||||
className={classNames('submit-button', classes.button)}
|
||||
disabled={!isChanged()}
|
||||
>
|
||||
{submitLabel ? submitLabel : <FormattedMessage id="forms.submit"/>}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
FormSubmit.propTypes = {
|
||||
submitLabel: PropTypes.node,
|
||||
cancelLabel: PropTypes.node,
|
||||
revertLabel: PropTypes.node,
|
||||
cancelCallback: PropTypes.func,
|
||||
revertCallback: PropTypes.func,
|
||||
document: PropTypes.object,
|
||||
deleteDocument: PropTypes.func,
|
||||
collectionName: PropTypes.string,
|
||||
classes: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
FormSubmit.contextTypes = {
|
||||
intl: intlShape,
|
||||
isChanged: PropTypes.func,
|
||||
clearForm: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
replaceComponent('FormSubmit', FormSubmit, [withStyles, styles]);
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { instantiateComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from 'mdi-material-ui/CloseCircle';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
export const styles = theme => ({
|
||||
inputAdornment: {
|
||||
whiteSpace: 'nowrap',
|
||||
marginTop: '0 !important',
|
||||
'& > *': {
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
'& > svg': {
|
||||
color: theme.palette.common.darkBlack,
|
||||
},
|
||||
'& > * + *': {
|
||||
marginLeft: 8,
|
||||
}
|
||||
},
|
||||
clearButton: {
|
||||
opacity: 0,
|
||||
'& svg': {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
marginRight: -12,
|
||||
marginLeft: -4,
|
||||
'&:first-child': {
|
||||
marginLeft: -12,
|
||||
},
|
||||
transition: theme.transitions.create('opacity', {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
urlButton: {
|
||||
verticalAlign: 'bottom',
|
||||
width: 24,
|
||||
height: 24,
|
||||
fontSize: 20,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const EndAdornment = (props) => {
|
||||
const { classes, value, addonAfter, changeValue, hideClear, disabled } = props;
|
||||
|
||||
if (!addonAfter && (!changeValue || hideClear || disabled)) return null;
|
||||
const hasValue = !!value || value === 0;
|
||||
|
||||
const clearButton = changeValue && !hideClear && !disabled &&
|
||||
<IconButton className={classNames('clear-button', classes.clearButton, hasValue && 'clear-enabled')}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
changeValue(null);
|
||||
}}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<CloseIcon/>
|
||||
</IconButton>;
|
||||
|
||||
return (
|
||||
<InputAdornment classes={{ root: classes.inputAdornment }} position="end">
|
||||
{instantiateComponent(addonAfter)}
|
||||
{clearButton}
|
||||
</InputAdornment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
EndAdornment.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
changeValue: PropTypes.func,
|
||||
hideClear: PropTypes.bool,
|
||||
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
|
||||
};
|
||||
|
||||
|
||||
export default withStyles(styles)(EndAdornment);
|
|
@ -0,0 +1,150 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ComponentMixin from './mixins/component';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import MuiFormControl from './MuiFormControl';
|
||||
import MuiFormHelper from './MuiFormHelper';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
group: {
|
||||
marginTop: '8px',
|
||||
},
|
||||
twoColumn: {
|
||||
display: 'block',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
'& > label': {
|
||||
marginRight: theme.spacing.unit * 5,
|
||||
},
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
'& > label': {
|
||||
width: '49%',
|
||||
},
|
||||
},
|
||||
},
|
||||
threeColumn: {
|
||||
display: 'block',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
'& > label': {
|
||||
marginRight: theme.spacing.unit * 5,
|
||||
},
|
||||
},
|
||||
[theme.breakpoints.up('xs')]: {
|
||||
'& > label': {
|
||||
width: '49%',
|
||||
},
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
'& > label': {
|
||||
width: '32%',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const MuiCheckboxGroup = createReactClass({
|
||||
|
||||
mixins: [ComponentMixin],
|
||||
|
||||
propTypes: {
|
||||
name: PropTypes.string.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
variant: PropTypes.oneOf(['checkbox', 'switch']),
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
if (this.props.refFunction) {
|
||||
this.props.refFunction(this);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
label: '',
|
||||
help: null,
|
||||
variant: 'checkbox',
|
||||
};
|
||||
},
|
||||
|
||||
changeCheckbox: function () {
|
||||
const value = [];
|
||||
this.props.options.forEach(function (option, key) {
|
||||
if (this[this.props.name + '-' + option.value].checked) {
|
||||
value.push(option.value);
|
||||
}
|
||||
}.bind(this));
|
||||
//this.setValue(value);
|
||||
this.props.onChange(this.props.name, value);
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
renderElement: function () {
|
||||
const controls = this.props.options.map((checkbox, key) => {
|
||||
let value = checkbox.value;
|
||||
let checked = (this.props.value.indexOf(value) !== -1);
|
||||
let disabled = checkbox.disabled || this.props.disabled;
|
||||
const Component = this.props.variant === 'switch' ? Switch : Checkbox;
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
control={
|
||||
<Component
|
||||
inputRef={(c) => this[this.props.name + '-' + value] = c}
|
||||
checked={checked}
|
||||
onChange={this.changeCheckbox}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
label={checkbox.label}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const maxLength = this.props.options.reduce((max, option) =>
|
||||
option.label.length > max ? option.label.length : max, 0);
|
||||
|
||||
const columnClass = maxLength < 20 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '';
|
||||
|
||||
return (
|
||||
<FormGroup className={classNames(this.props.classes.group, this.props.classes[columnClass])}>
|
||||
{controls}
|
||||
</FormGroup>
|
||||
);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
|
||||
if (this.props.layout === 'elementOnly') {
|
||||
return (
|
||||
<div>{this.renderElement()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiFormControl{...this.getFormControlProperties()} fakeLabel={true}>
|
||||
{this.renderElement()}
|
||||
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||
</MuiFormControl>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default withStyles(styles)(MuiCheckboxGroup);
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
|
||||
|
||||
//noinspection JSUnusedGlobalSymbols
|
||||
const MuiFormControl = createReactClass({
|
||||
|
||||
propTypes: {
|
||||
label: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
required: PropTypes.bool,
|
||||
hasErrors: PropTypes.bool,
|
||||
fakeLabel: PropTypes.bool,
|
||||
hideLabel: PropTypes.bool,
|
||||
layout: PropTypes.oneOf(['horizontal', 'vertical', 'elementOnly']),
|
||||
htmlFor: PropTypes.string
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
label: '',
|
||||
required: false,
|
||||
hasErrors: false,
|
||||
fakeLabel: false,
|
||||
hideLabel: false,
|
||||
};
|
||||
},
|
||||
|
||||
renderRequiredSymbol: function () {
|
||||
if (this.props.required === false) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className="required-symbol"> *</span>
|
||||
);
|
||||
},
|
||||
|
||||
renderLabel: function () {
|
||||
if (this.props.layout === 'elementOnly' || this.props.hideLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.props.fakeLabel) {
|
||||
return (
|
||||
<FormLabel className="control-label legend"
|
||||
component="legend"
|
||||
data-required={this.props.required}
|
||||
>
|
||||
{this.props.label}
|
||||
{this.renderRequiredSymbol()}
|
||||
</FormLabel>
|
||||
);
|
||||
}
|
||||
|
||||
const shrink = ['date', 'time', 'datetime'].includes(this.props.inputType) ? true : undefined;
|
||||
|
||||
return (
|
||||
<InputLabel className="control-label"
|
||||
data-required={this.props.required}
|
||||
htmlFor={this.props.htmlFor}
|
||||
shrink={shrink}
|
||||
>
|
||||
{this.props.label}
|
||||
{this.renderRequiredSymbol()}
|
||||
</InputLabel>
|
||||
);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
const { layout, className, children, hasErrors } = this.props;
|
||||
|
||||
if (layout === 'elementOnly') {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl component="fieldset" error={hasErrors} fullWidth={true} className={className}>
|
||||
{this.renderLabel()}
|
||||
{children}
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
export default MuiFormControl;
|
|
@ -0,0 +1,75 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { instantiateComponent, Components } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
export const styles = theme => ({
|
||||
|
||||
error: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
|
||||
formHelperText: {
|
||||
display: 'flex',
|
||||
'& :first-child': {
|
||||
flexGrow: 1,
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const MuiFormHelper = (props) => {
|
||||
const {
|
||||
classes,
|
||||
help,
|
||||
errors,
|
||||
hasErrors,
|
||||
showCharsRemaining,
|
||||
charsRemaining,
|
||||
charsCount,
|
||||
max,
|
||||
} = props;
|
||||
|
||||
if (!help && !hasErrors && !showCharsRemaining) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorMessage = hasErrors &&
|
||||
<Components.FormError error={errors[0]} />;
|
||||
|
||||
return (
|
||||
<FormHelperText className={classes.formHelperText} error={hasErrors}>
|
||||
|
||||
<span>
|
||||
{
|
||||
hasErrors ? errorMessage : help
|
||||
}
|
||||
</span>
|
||||
|
||||
{
|
||||
showCharsRemaining &&
|
||||
|
||||
<span className={charsRemaining < 0 ? classes.error : null}>
|
||||
{charsCount} / {max}
|
||||
</span>
|
||||
}
|
||||
|
||||
</FormHelperText>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
MuiFormHelper.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
changeValue: PropTypes.func,
|
||||
addonAfter: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
|
||||
};
|
||||
|
||||
|
||||
export default withStyles(styles)(MuiFormHelper);
|
|
@ -0,0 +1,138 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import ComponentMixin from './mixins/component';
|
||||
import MuiFormControl from './MuiFormControl';
|
||||
import MuiFormHelper from './MuiFormHelper';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import StartAdornment, { hideStartAdornment, fixUrl } from './StartAdornment';
|
||||
import EndAdornment from './EndAdornment';
|
||||
|
||||
|
||||
export const styles = theme => ({
|
||||
inputRoot: {
|
||||
'& .clear-enabled': { opacity: 0 },
|
||||
'&:hover .clear-enabled': { opacity: 0.54 },
|
||||
},
|
||||
inputFocused: {
|
||||
'& .clear-enabled': { opacity: 0.54 }
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
//noinspection JSUnusedGlobalSymbols
|
||||
const MuiInput = createReactClass({
|
||||
element: null,
|
||||
|
||||
mixins: [ComponentMixin],
|
||||
|
||||
displayName: 'MuiInput',
|
||||
|
||||
propTypes: {
|
||||
type: PropTypes.oneOf([
|
||||
'color',
|
||||
'date',
|
||||
'datetime',
|
||||
'datetime-local',
|
||||
'email',
|
||||
'hidden',
|
||||
'month',
|
||||
'number',
|
||||
'password',
|
||||
'range',
|
||||
'search',
|
||||
'tel',
|
||||
'text',
|
||||
'time',
|
||||
'url',
|
||||
'week'
|
||||
]),
|
||||
errors: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
formatValue: PropTypes.func,
|
||||
scrubValue: PropTypes.func,
|
||||
hideClear: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
type: 'text',
|
||||
};
|
||||
},
|
||||
|
||||
handleChange: function (event) {
|
||||
let value = event.target.value;
|
||||
if (this.props.scrubValue) {
|
||||
value = this.props.scrubValue(value);
|
||||
}
|
||||
this.changeValue(value);
|
||||
},
|
||||
|
||||
changeValue: function (value) {
|
||||
this.props.onChange(this.props.name, value);
|
||||
},
|
||||
|
||||
handleBlur: function (event) {
|
||||
const { type, value } = this.props;
|
||||
|
||||
if (type === 'url' && !!value && value !== fixUrl(value)) {
|
||||
this.changeValue(fixUrl(value))
|
||||
}
|
||||
},
|
||||
|
||||
render: function () {
|
||||
const startAdornment = hideStartAdornment(this.props) ? null :
|
||||
<StartAdornment {...this.props}
|
||||
classes={null}
|
||||
changeValue={this.changeValue}
|
||||
/>;
|
||||
const endAdornment =
|
||||
<EndAdornment {...this.props}
|
||||
classes={null}
|
||||
changeValue={this.changeValue}
|
||||
/>;
|
||||
|
||||
let element = this.renderElement(startAdornment, endAdornment);
|
||||
|
||||
if (this.props.layout === 'elementOnly' || this.props.type === 'hidden') {
|
||||
return element;
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiFormControl {...this.getFormControlProperties()} htmlFor={this.getId()}>
|
||||
{element}
|
||||
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||
</MuiFormControl>
|
||||
);
|
||||
},
|
||||
|
||||
renderElement: function (startAdornment, endAdornment) {
|
||||
const { classes, disabled, autoFocus, formatValue } = this.props;
|
||||
const value = formatValue ? formatValue(this.props.value) : this.props.value;
|
||||
const options = this.props.options || {};
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={c => (this.element = c)}
|
||||
{...this.cleanProps(this.props)}
|
||||
id={this.getId()}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
disabled={disabled}
|
||||
rows={options.rows || this.props.rows}
|
||||
autoFocus={options.autoFocus || autoFocus}
|
||||
startAdornment={startAdornment}
|
||||
endAdornment={endAdornment}
|
||||
placeholder={this.props.placeholder}
|
||||
classes={{ root: classes.inputRoot, focused: classes.inputFocused }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
export default withStyles(styles)(MuiInput);
|
|
@ -0,0 +1,162 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ComponentMixin from './mixins/component';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import MuiFormControl from './MuiFormControl';
|
||||
import MuiFormHelper from './MuiFormHelper';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Radio from '@material-ui/core/Radio';
|
||||
import RadioGroup from '@material-ui/core/RadioGroup';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
group: {
|
||||
marginTop: '8px',
|
||||
},
|
||||
inline: {
|
||||
flexDirection: 'row',
|
||||
'& > label': {
|
||||
marginRight: theme.spacing.unit * 5,
|
||||
},
|
||||
},
|
||||
twoColumn: {
|
||||
display: 'block',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
'& > label': {
|
||||
marginRight: theme.spacing.unit * 5,
|
||||
},
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
'& > label': {
|
||||
width: '49%',
|
||||
},
|
||||
},
|
||||
},
|
||||
threeColumn: {
|
||||
display: 'block',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
'& > label': {
|
||||
marginRight: theme.spacing.unit * 5,
|
||||
},
|
||||
},
|
||||
[theme.breakpoints.up('xs')]: {
|
||||
'& > label': {
|
||||
width: '49%',
|
||||
},
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
'& > label': {
|
||||
width: '32%',
|
||||
},
|
||||
},
|
||||
},
|
||||
radio: {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
marginLeft: '8px',
|
||||
},
|
||||
line: {
|
||||
marginBottom: '12px',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const MuiRadioGroup = createReactClass({
|
||||
|
||||
mixins: [ComponentMixin],
|
||||
|
||||
propTypes: {
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['inline', 'stacked']),
|
||||
options: PropTypes.array.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function () {
|
||||
if (this.props.refFunction) {
|
||||
this.props.refFunction(this);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
type: 'stacked',
|
||||
label: '',
|
||||
help: null,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
},
|
||||
|
||||
changeRadio: function (event) {
|
||||
const value = event.target.value;
|
||||
//this.setValue(value);
|
||||
this.props.onChange(this.props.name, value);
|
||||
},
|
||||
|
||||
validate: function () {
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
renderElement: function () {
|
||||
const controls = this.props.options.map((radio, key) => {
|
||||
let checked = (this.props.value === radio.value);
|
||||
let disabled = radio.disabled || this.props.disabled;
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
value={radio.value}
|
||||
control={<Radio
|
||||
className={this.props.classes.radio}
|
||||
inputRef={(c) => this['element-' + key] = c}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
/>}
|
||||
className={this.props.classes.line}
|
||||
label={radio.label}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const maxLength = this.props.options.reduce((max, option) =>
|
||||
option.label.length > max ? option.label.length : max, 0);
|
||||
|
||||
let columnClass = maxLength < 18 ? 'threeColumn' : maxLength < 30 ? 'twoColumn' : '';
|
||||
if (this.props.type === 'inline') columnClass = 'inline';
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
aria-label={this.props.name}
|
||||
name={this.props.name}
|
||||
className={classNames(this.props.classes.group, this.props.classes[columnClass])}
|
||||
value={this.props.value}
|
||||
onChange={this.changeRadio}
|
||||
>
|
||||
{controls}
|
||||
</RadioGroup>
|
||||
);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
|
||||
if (this.props.layout === 'elementOnly') {
|
||||
return (
|
||||
<div>{this.renderElement()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiFormControl{...this.getFormControlProperties()} fakeLabel={true}>
|
||||
{this.renderElement()}
|
||||
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||
</MuiFormControl>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default withStyles(styles)(MuiRadioGroup);
|
|
@ -0,0 +1,210 @@
|
|||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ComponentMixin from './mixins/component';
|
||||
import MuiFormControl from './MuiFormControl';
|
||||
import MuiFormHelper from './MuiFormHelper';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import MenuList from '@material-ui/core/MenuList';
|
||||
import ListSubheader from '@material-ui/core/ListSubheader';
|
||||
import StartAdornment, { hideStartAdornment } from './StartAdornment';
|
||||
import EndAdornment from './EndAdornment';
|
||||
import _isArray from 'lodash/isArray';
|
||||
|
||||
|
||||
export const styles = theme => ({
|
||||
|
||||
inputRoot: {
|
||||
'& .clear-enabled': { opacity: 0 },
|
||||
'&:hover .clear-enabled': { opacity: 0.54 },
|
||||
},
|
||||
|
||||
inputFocused: {
|
||||
'& .clear-enabled': { opacity: 0.54 }
|
||||
},
|
||||
|
||||
menuItem: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 9,
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
color: theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.87)' : theme.palette.common.white,
|
||||
fontSize: theme.typography.pxToRem(16),
|
||||
lineHeight: '1.1875em',
|
||||
},
|
||||
|
||||
input: {
|
||||
paddingLeft: 8,
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const MuiSelect = createReactClass({
|
||||
|
||||
element: null,
|
||||
|
||||
mixins: [ComponentMixin],
|
||||
|
||||
getInitialState: function () {
|
||||
return {
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
handleOpen: function () {
|
||||
// this doesn't work
|
||||
this.setState({
|
||||
isOpen: true,
|
||||
});
|
||||
},
|
||||
|
||||
handleClose: function () {
|
||||
// this doesn't work
|
||||
this.setState({
|
||||
isOpen: false,
|
||||
});
|
||||
},
|
||||
|
||||
handleChange: function (event) {
|
||||
const target = event.target;
|
||||
let value;
|
||||
if (this.props.multiple) {
|
||||
value = [];
|
||||
for (let i = 0; i < target.length; i++) {
|
||||
const option = target.options[i];
|
||||
if (option.selected) {
|
||||
value.push(option.value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = target.value;
|
||||
}
|
||||
this.changeValue(value);
|
||||
},
|
||||
|
||||
changeValue: function (value) {
|
||||
this.props.onChange(this.props.name, value);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
if (this.props.layout === 'elementOnly') {
|
||||
return this.renderElement();
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiFormControl{...this.getFormControlProperties()} htmlFor={this.getId()}>
|
||||
{this.renderElement()}
|
||||
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||
</MuiFormControl>
|
||||
);
|
||||
},
|
||||
|
||||
renderElement: function () {
|
||||
const renderOption = (item, key) => {
|
||||
//eslint-disable-next-line no-unused-vars
|
||||
const { group, label, ...rest } = item;
|
||||
return this.props.native
|
||||
?
|
||||
<option key={key} {...rest}>{label}</option>
|
||||
:
|
||||
<MenuItem key={key} {...rest} className={classes.menuItem}>{label}</MenuItem>;
|
||||
};
|
||||
|
||||
const renderGroup = (label, key, nodes) => {
|
||||
return this.props.native
|
||||
?
|
||||
<optgroup label={label} key={key}>
|
||||
{nodes}
|
||||
</optgroup>
|
||||
:
|
||||
<MenuList subheader={<ListSubheader component="div">{label}</ListSubheader>} key={key}>
|
||||
{nodes}
|
||||
</MenuList>;
|
||||
};
|
||||
|
||||
const { options, classes } = this.props;
|
||||
|
||||
let groups = options.filter(function (item) {
|
||||
return item.group;
|
||||
}).map(function (item) {
|
||||
return item.group;
|
||||
});
|
||||
// Get the unique items in group.
|
||||
groups = [...new Set(groups)];
|
||||
|
||||
let optionNodes = [];
|
||||
|
||||
if (groups.length === 0) {
|
||||
optionNodes = options.map(function (item, index) {
|
||||
return renderOption(item, index);
|
||||
});
|
||||
} else {
|
||||
// For items without groups.
|
||||
const itemsWithoutGroup = options.filter(function (item) {
|
||||
return !item.group;
|
||||
});
|
||||
|
||||
itemsWithoutGroup.forEach(function (item, index) {
|
||||
optionNodes.push(renderOption(item, 'no-group-' + index));
|
||||
});
|
||||
|
||||
groups.forEach(function (group, groupIndex) {
|
||||
|
||||
const groupItems = options.filter(function (item) {
|
||||
return item.group === group;
|
||||
});
|
||||
|
||||
const groupOptionNodes = groupItems.map(function (item, index) {
|
||||
return renderOption(item, groupIndex + '-' + index);
|
||||
});
|
||||
|
||||
optionNodes.push(renderGroup(group, groupIndex, groupOptionNodes));
|
||||
});
|
||||
}
|
||||
|
||||
let value = this.props.value;
|
||||
if (!this.props.multiple && _isArray(value)) {
|
||||
value = value.length ? value[0] : '';
|
||||
}
|
||||
|
||||
const startAdornment = hideStartAdornment(this.props) ? null :
|
||||
<StartAdornment {...this.props}
|
||||
value={value}
|
||||
classes={null}
|
||||
/>;
|
||||
const endAdornment =
|
||||
<EndAdornment {...this.props}
|
||||
value={value}
|
||||
classes={null}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<Select className="select"
|
||||
ref={(c) => this.element = c}
|
||||
{...this.cleanProps(this.props)}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onOpen={this.handleOpen}
|
||||
onClose={this.handleClose}
|
||||
disabled={this.props.disabled}
|
||||
input={<Input id={this.getId()}
|
||||
startAdornment={startAdornment}
|
||||
endAdornment={endAdornment}
|
||||
classes={{
|
||||
root: classes.inputRoot,
|
||||
focused: classes.inputFocused,
|
||||
input: classes.input,
|
||||
}}
|
||||
/>}
|
||||
>
|
||||
{optionNodes}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default withStyles(styles)(MuiSelect);
|
|
@ -0,0 +1,437 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ComponentMixin from './mixins/component';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import match from 'autosuggest-highlight/match';
|
||||
import parse from 'autosuggest-highlight/parse';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import StartAdornment, { hideStartAdornment } from './StartAdornment';
|
||||
import EndAdornment from './EndAdornment';
|
||||
import MuiFormControl from './MuiFormControl';
|
||||
import MuiFormHelper from './MuiFormHelper';
|
||||
import _isEqual from 'lodash/isEqual';
|
||||
import classNames from 'classnames';
|
||||
import IsolatedScroll from 'react-isolated-scroll';
|
||||
|
||||
|
||||
const maxSuggestions = 100;
|
||||
|
||||
|
||||
/*{
|
||||
container: 'react-autosuggest__container',
|
||||
containerOpen: 'react-autosuggest__container--open',
|
||||
input: 'react-autosuggest__input',
|
||||
inputOpen: 'react-autosuggest__input--open',
|
||||
inputFocused: 'react-autosuggest__input--focused',
|
||||
suggestionsContainer: 'react-autosuggest__suggestions-container',
|
||||
suggestionsContainerOpen: 'react-autosuggest__suggestions-container--open',
|
||||
suggestionsList: 'react-autosuggest__suggestions-list',
|
||||
suggestion: 'react-autosuggest__suggestion',
|
||||
suggestionFirst: 'react-autosuggest__suggestion--first',
|
||||
suggestionHighlighted: 'react-autosuggest__suggestion--highlighted',
|
||||
sectionContainer: 'react-autosuggest__section-container',
|
||||
sectionContainerFirst: 'react-autosuggest__section-container--first',
|
||||
sectionTitle: 'react-autosuggest__section-title'
|
||||
}*/
|
||||
const styles = theme => ({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
},
|
||||
textField: {
|
||||
width: '100%',
|
||||
'label + div > &': {
|
||||
marginTop: theme.spacing.unit * 2,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
outline: 0,
|
||||
font: 'inherit',
|
||||
color: 'currentColor',
|
||||
width: '100%',
|
||||
border: '0',
|
||||
margin: '0',
|
||||
padding: '7px 0',
|
||||
display: 'block',
|
||||
boxSizing: 'content-box',
|
||||
background: 'none',
|
||||
verticalAlign: 'middle',
|
||||
'&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::after, &:after':
|
||||
{ display: 'none' },
|
||||
'&::-webkit-search-results, &::-webkit-search-results-decoration':
|
||||
{ display: 'none' },
|
||||
},
|
||||
readOnly: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
suggestionsContainer: {
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: theme.zIndex.modal,
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
maxHeight: 48 * 8,
|
||||
},
|
||||
suggestionsContainerOpen: {
|
||||
display: 'flex',
|
||||
},
|
||||
scroller: {
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
suggestion: {
|
||||
display: 'block',
|
||||
},
|
||||
suggestionIcon: {
|
||||
marginRight: theme.spacing.unit * 2,
|
||||
},
|
||||
selected: {
|
||||
backgroundColor: theme.palette.secondary.light,
|
||||
},
|
||||
suggestionsList: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyleType: 'none',
|
||||
},
|
||||
inputRoot: {
|
||||
'& .clear-enabled': { opacity: 0 },
|
||||
'&:hover .clear-enabled': { opacity: 0.54 },
|
||||
},
|
||||
inputFocused: {
|
||||
'& .clear-enabled': { opacity: 0.54 }
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const MuiSuggest = createReactClass({
|
||||
|
||||
inputElement: null,
|
||||
|
||||
mixins: [ComponentMixin],
|
||||
|
||||
propTypes: {
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
formatted: PropTypes.node,
|
||||
iconComponent: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
})),
|
||||
classes: PropTypes.object.isRequired,
|
||||
limitToList: PropTypes.bool,
|
||||
disableText: PropTypes.bool,
|
||||
showAllOptions: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
autoComplete: 'off',
|
||||
autoFocus: false,
|
||||
};
|
||||
},
|
||||
|
||||
getOptionFormatted: function (option) {
|
||||
return option.formatted || option.label || option.value || '';
|
||||
},
|
||||
|
||||
getOptionLabel: function (option) {
|
||||
return option.label || option.value || '';
|
||||
},
|
||||
|
||||
getInitialState: function () {
|
||||
if (this.props.refFunction) {
|
||||
this.props.refFunction(this);
|
||||
}
|
||||
|
||||
const selectedOption = this.getSelectedOption();
|
||||
return {
|
||||
inputValue: this.getOptionLabel(selectedOption),
|
||||
selectedOption: selectedOption,
|
||||
suggestions: [],
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
if (nextProps.value !== this.state.value ||
|
||||
nextProps.options !== this.props.options) {
|
||||
const selectedOption = this.getSelectedOption(nextProps);
|
||||
this.setState({
|
||||
inputValue: this.getOptionLabel(selectedOption),
|
||||
selectedOption: selectedOption,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function (nextProps, nextState) {
|
||||
return !_isEqual(nextState, this.state) ||
|
||||
nextProps.help !== this.props.help ||
|
||||
nextProps.charsCount !== this.props.charsCount ||
|
||||
!_isEqual(nextProps.errors, this.props.errors) ||
|
||||
nextProps.options !== this.props.options;
|
||||
},
|
||||
|
||||
getSelectedOption: function (props) {
|
||||
props = props || this.props;
|
||||
const selectedOption = props.options.find((opt) => opt.value === props.value);
|
||||
return selectedOption || { label: '', value: null };
|
||||
},
|
||||
|
||||
handleFocus: function (event) {
|
||||
if (!this.inputElement) return;
|
||||
|
||||
this.inputElement.select();
|
||||
},
|
||||
|
||||
handleBlur: function (event, { highlightedSuggestion: suggestion }) {
|
||||
if (suggestion) {
|
||||
this.changeValue(suggestion);
|
||||
} else if (this.props.limitToList) {
|
||||
const selectedOption = this.getSelectedOption();
|
||||
this.setState({
|
||||
inputValue: this.getOptionLabel(selectedOption),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
suggestionSelected: function (event, { suggestion }) {
|
||||
this.changeValue(suggestion);
|
||||
},
|
||||
|
||||
changeValue: function (suggestion) {
|
||||
if (!suggestion) {
|
||||
suggestion = { label: '', value: null };
|
||||
}
|
||||
if (suggestion.onClick) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedOption: suggestion,
|
||||
inputValue: this.getOptionLabel(suggestion),
|
||||
});
|
||||
this.props.onChange(this.props.name, suggestion.value, this.getOptionLabel(suggestion));
|
||||
},
|
||||
|
||||
handleInputChange: function (event) {
|
||||
const value = event.target.value;
|
||||
this.setState({
|
||||
inputValue: value,
|
||||
});
|
||||
},
|
||||
|
||||
handleSuggestionsFetchRequested: function ({ value, reason }) {
|
||||
this.setState({
|
||||
suggestions: this.getSuggestions(value),
|
||||
});
|
||||
},
|
||||
|
||||
handleSuggestionsClearRequested: function () {
|
||||
this.setState({
|
||||
suggestions: [],
|
||||
});
|
||||
},
|
||||
|
||||
shouldRenderSuggestions: function (value) {
|
||||
return true;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
const value = this.props.value;
|
||||
|
||||
const startAdornment = hideStartAdornment(this.props) ? null :
|
||||
<StartAdornment {...this.props}
|
||||
value={value}
|
||||
classes={null}
|
||||
/>;
|
||||
const endAdornment =
|
||||
<EndAdornment {...this.props}
|
||||
value={value}
|
||||
classes={null}
|
||||
changeValue={this.changeValue}
|
||||
/>;
|
||||
|
||||
const element = this.renderElement(startAdornment, endAdornment);
|
||||
|
||||
if (this.props.layout === 'elementOnly') {
|
||||
return element;
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiFormControl{...this.getFormControlProperties()} htmlFor={this.getId()}>
|
||||
{element}
|
||||
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||
</MuiFormControl>
|
||||
);
|
||||
},
|
||||
|
||||
renderElement: function (startAdornment, endAdornment) {
|
||||
const { classes, autoFocus, disableText, showAllOptions } = this.props;
|
||||
|
||||
return (
|
||||
<Autosuggest
|
||||
theme={{
|
||||
container: classes.container,
|
||||
input: classNames(classes.input, this.props.disableText && classes.readOnly),
|
||||
suggestionsContainer: classes.suggestionsContainer,
|
||||
suggestionsContainerOpen: classes.suggestionsContainerOpen,
|
||||
suggestion: classes.suggestion,
|
||||
suggestionsList: classes.suggestionsList,
|
||||
}}
|
||||
highlightFirstSuggestion={!disableText && !showAllOptions}
|
||||
renderInputComponent={this.renderInputComponent}
|
||||
suggestions={this.state.suggestions}
|
||||
onSuggestionsFetchRequested={this.handleSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.handleSuggestionsClearRequested}
|
||||
renderSuggestionsContainer={this.renderSuggestionsContainer}
|
||||
shouldRenderSuggestions={this.shouldRenderSuggestions}
|
||||
focusInputOnSuggestionClick={false}
|
||||
alwaysRenderSuggestions={false}
|
||||
getSuggestionValue={this.getSuggestionValue}
|
||||
renderSuggestion={this.renderSuggestion}
|
||||
onSuggestionSelected={this.suggestionSelected}
|
||||
inputProps={{
|
||||
autoFocus,
|
||||
classes,
|
||||
onChange: this.handleInputChange,
|
||||
onFocus: this.handleFocus,
|
||||
onBlur: this.handleBlur,
|
||||
value: this.state.inputValue,
|
||||
readOnly: this.props.disableText,
|
||||
disabled: this.props.disabled,
|
||||
name: this.props.name,
|
||||
'aria-haspopup': 'true',
|
||||
...this.props.inputProps,
|
||||
startAdornment,
|
||||
endAdornment,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
renderInputComponent: function (inputProps) {
|
||||
const { classes, autoFocus, autoComplete, value, ref, startAdornment, endAdornment, disabled, ...rest } = inputProps;
|
||||
|
||||
return (
|
||||
<Input
|
||||
autoFocus={autoFocus}
|
||||
autoComplete={autoComplete}
|
||||
className={classes.textField}
|
||||
classes={{ root: classes.inputRoot, focused: classes.inputFocused }}
|
||||
value={value}
|
||||
inputRef={c => { ref(c); this.inputElement = c; }}
|
||||
type="text"
|
||||
startAdornment={startAdornment}
|
||||
endAdornment={endAdornment}
|
||||
disabled={disabled}
|
||||
inputProps={{
|
||||
...rest,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
renderSuggestion: function (suggestion, { query, isHighlighted }) {
|
||||
const label = this.getOptionFormatted(suggestion);
|
||||
const matches = match(label, query);
|
||||
const parts = parse(label, matches);
|
||||
const isSelected = suggestion.value === this.props.value;
|
||||
const className = isSelected ? this.props.classes.selected : null;
|
||||
|
||||
return (
|
||||
<MenuItem selected={isHighlighted}
|
||||
component="div"
|
||||
className={className}
|
||||
onClick={suggestion.onClick}
|
||||
data-value={suggestion.value}
|
||||
>
|
||||
{
|
||||
suggestion.iconComponent &&
|
||||
<div className={this.props.classes.suggestionIcon}>
|
||||
{suggestion.iconComponent}
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
{parts.map((part, index) => {
|
||||
return part.highlight ? (
|
||||
<span key={index} style={{ fontWeight: 500 }}>
|
||||
{part.text}
|
||||
</span>
|
||||
) : (
|
||||
<strong key={index} style={{ fontWeight: 300 }}>
|
||||
{part.text}
|
||||
</strong>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
},
|
||||
|
||||
renderSuggestionsContainer: function ({ containerProps, children }) {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<Paper {...containerProps} id={`menu-${this.props.name}`} square>
|
||||
<IsolatedScroll className={classes.scroller}>
|
||||
{children}
|
||||
</IsolatedScroll>
|
||||
</Paper>
|
||||
);
|
||||
},
|
||||
|
||||
getSuggestionValue: function (suggestion) {
|
||||
return suggestion.value;
|
||||
},
|
||||
|
||||
getSuggestions: function (value) {
|
||||
const inputValue = value.trim().toLowerCase();
|
||||
const inputLength = inputValue.length;
|
||||
let count = 0;
|
||||
const inputMatchesSelection = value === this.getOptionLabel(this.state.selectedOption);
|
||||
|
||||
return (this.props.disableText || this.props.showAllOptions) && inputMatchesSelection ?
|
||||
|
||||
this.props.options.filter(suggestion => {
|
||||
return true;
|
||||
})
|
||||
|
||||
:
|
||||
|
||||
inputLength === 0
|
||||
|
||||
?
|
||||
|
||||
this.props.options.filter(suggestion => {
|
||||
count += 1;
|
||||
return count <= maxSuggestions;
|
||||
})
|
||||
|
||||
:
|
||||
|
||||
this.props.options.filter(suggestion => {
|
||||
const label = this.getOptionLabel(suggestion);
|
||||
const keep =
|
||||
count < maxSuggestions && label.toLowerCase().slice(0, inputLength) ===
|
||||
inputValue;
|
||||
|
||||
if (keep) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return keep;
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
export default withStyles(styles)(MuiSuggest);
|
||||
registerComponent('MuiSuggest', MuiSuggest, [withStyles, styles]);
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ComponentMixin from './mixins/component';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import MuiFormControl from './MuiFormControl';
|
||||
import MuiFormHelper from './MuiFormHelper';
|
||||
|
||||
|
||||
const MuiSwitch = createReactClass({
|
||||
|
||||
mixins: [ComponentMixin],
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
label: '',
|
||||
rowLabel: '',
|
||||
value: false
|
||||
};
|
||||
},
|
||||
|
||||
changeValue: function (event) {
|
||||
const target = event.target;
|
||||
const value = target.checked;
|
||||
|
||||
//this.setValue(value);
|
||||
this.props.onChange(this.props.name, value);
|
||||
|
||||
setTimeout(() => {document.activeElement.blur();});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
|
||||
const element = this.renderElement();
|
||||
|
||||
if (this.props.layout === 'elementOnly') {
|
||||
return element;
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiFormControl {...this.getFormControlProperties()} label={this.props.rowLabel}
|
||||
htmlFor={this.getId()}
|
||||
>
|
||||
{element}
|
||||
<MuiFormHelper {...this.getFormHelperProperties()}/>
|
||||
</MuiFormControl>
|
||||
);
|
||||
},
|
||||
|
||||
renderElement: function () {
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
ref={(c) => this.element = c}
|
||||
{...this.cleanSwitchProps(this.cleanProps(this.props))}
|
||||
id={this.getId()}
|
||||
checked={this.props.value === true}
|
||||
onChange={this.changeValue}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
}
|
||||
label={this.props.label}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
export default MuiSwitch;
|
|
@ -0,0 +1,54 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { instantiateComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import OpenInNewIcon from 'mdi-material-ui/OpenInNew';
|
||||
import { styles } from './EndAdornment';
|
||||
|
||||
|
||||
export const hideStartAdornment = (props) => {
|
||||
return !props.addonBefore && !props.isUrl;
|
||||
};
|
||||
|
||||
|
||||
export const fixUrl = (url) => {
|
||||
return url.indexOf('http://') === -1 && url.indexOf('https://') ? 'http://' + url : url;
|
||||
};
|
||||
|
||||
|
||||
const StartAdornment = (props) => {
|
||||
const { classes, value, type, addonBefore } = props;
|
||||
|
||||
if (hideStartAdornment(props)) return null;
|
||||
|
||||
const urlButton = type === 'url' &&
|
||||
<IconButton
|
||||
className={classes.urlButton}
|
||||
href={fixUrl(value)}
|
||||
target="_blank"
|
||||
disabled={!value}
|
||||
>
|
||||
<OpenInNewIcon/>
|
||||
</IconButton>;
|
||||
|
||||
|
||||
return (
|
||||
<InputAdornment classes={{ root: classes.inputAdornment }} position="start">
|
||||
{instantiateComponent(addonBefore)}
|
||||
{urlButton}
|
||||
</InputAdornment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
StartAdornment.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
type: PropTypes.string,
|
||||
addonBefore: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
|
||||
};
|
||||
|
||||
|
||||
export default withStyles(styles)(StartAdornment);
|
|
@ -0,0 +1,139 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
import _omit from 'lodash/omit';
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
propTypes: {
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
hideLabel: PropTypes.bool,
|
||||
layout: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
},
|
||||
|
||||
getFormControlProperties: function () {
|
||||
return {
|
||||
label: this.props.label,
|
||||
hideLabel: this.props.hideLabel,
|
||||
layout: this.props.layout,
|
||||
required: this.props.required,
|
||||
hasErrors: this.hasErrors(),
|
||||
className: this.props.className,
|
||||
inputType: this.props.inputType,
|
||||
};
|
||||
},
|
||||
|
||||
getFormHelperProperties: function () {
|
||||
return {
|
||||
help: this.props.help,
|
||||
errors: this.props.errors,
|
||||
hasErrors: this.hasErrors(),
|
||||
showCharsRemaining: this.props.showCharsRemaining,
|
||||
charsRemaining: this.props.charsRemaining,
|
||||
charsCount: this.props.charsCount,
|
||||
max: this.props.max,
|
||||
};
|
||||
},
|
||||
|
||||
hashString: function (string) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
hash = (((hash << 5) - hash) + string.charCodeAt(i)) & 0xFFFFFFFF;
|
||||
}
|
||||
return hash;
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID is used as an attribute on the form control, and is used to allow
|
||||
* associating the label element with the form control.
|
||||
*
|
||||
* If we don't explicitly pass an `id` prop, we generate one based on the
|
||||
* `name`, `label` and `itemIndex` (for nested forms) properties.
|
||||
*/
|
||||
getId: function () {
|
||||
if (this.props.id) {
|
||||
return this.props.id;
|
||||
}
|
||||
const label = (typeof this.props.label === 'undefined' ? '' : this.props.label);
|
||||
const itemIndex = (typeof this.props.itemIndex=== 'undefined' ? '' : this.props.itemIndex);
|
||||
return [
|
||||
'frc',
|
||||
this.props.name.split('[').join('_').replace(']', ''),
|
||||
itemIndex,
|
||||
this.hashString(JSON.stringify(label))
|
||||
].join('-');
|
||||
},
|
||||
|
||||
hasErrors: function () {
|
||||
return !!(this.props.errors && this.props.errors.length);
|
||||
},
|
||||
|
||||
cleanProps: function (props) {
|
||||
const removedFields = [
|
||||
'beforeComponent',
|
||||
'afterComponent',
|
||||
'addonAfter',
|
||||
'addonBefore',
|
||||
'help',
|
||||
'label',
|
||||
'hideLabel',
|
||||
'options',
|
||||
'layout',
|
||||
'rowLabel',
|
||||
'validatePristine',
|
||||
'validateOnSubmit',
|
||||
'inputClassName',
|
||||
'optional',
|
||||
'throwError',
|
||||
'currentValues',
|
||||
'addToDeletedValues',
|
||||
'deletedValues',
|
||||
'clearFieldErrors',
|
||||
'formType',
|
||||
'inputType',
|
||||
'showCharsRemaining',
|
||||
'charsCount',
|
||||
'charsRemaining',
|
||||
'handleChange',
|
||||
'document',
|
||||
'updateCurrentValues',
|
||||
'classes',
|
||||
'errors',
|
||||
'description',
|
||||
'clearField',
|
||||
'regEx',
|
||||
'mustComplete',
|
||||
'renderComponent',
|
||||
'formInput',
|
||||
'className',
|
||||
'formatValue',
|
||||
'scrubValue',
|
||||
'custom',
|
||||
'hideClear',
|
||||
'inputProperties',
|
||||
'currentUser',
|
||||
'nestedSchema',
|
||||
'parentFieldName',
|
||||
'itemIndex',
|
||||
'formComponents',
|
||||
'autoValue',
|
||||
'minCount',
|
||||
'maxCount'
|
||||
];
|
||||
|
||||
return _omit(props, removedFields);
|
||||
},
|
||||
|
||||
cleanSwitchProps: function (props) {
|
||||
const removedFields = [
|
||||
'value',
|
||||
'error',
|
||||
];
|
||||
|
||||
return _omit(props, removedFields);
|
||||
},
|
||||
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import MuiSwitch from '../base-controls/MuiSwitch';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const CheckboxComponent = ({ refFunction, ...properties }) =>
|
||||
<MuiSwitch {...properties} ref={refFunction}/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentCheckbox', CheckboxComponent);
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import MuiCheckboxGroup from '../base-controls/MuiCheckboxGroup';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const CheckboxGroupComponent = ({ refFunction, ...properties }) =>
|
||||
<MuiCheckboxGroup {...properties} ref={refFunction}/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentCheckboxGroup', CheckboxGroupComponent);
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import MuiSuggest from '../base-controls/MuiSuggest';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import { countries } from './countries';
|
||||
|
||||
|
||||
const CountrySelect = ({ refFunction, ...properties }) =>
|
||||
<MuiSuggest {...properties} ref={refFunction} options={countries} limitToList={true}/>;
|
||||
|
||||
|
||||
registerComponent('CountrySelect', CountrySelect);
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
|
||||
|
||||
export const styles = theme => ({
|
||||
|
||||
'@global': {
|
||||
'input[type=date]::-ms-clear, input[type=date]::-ms-reveal': {
|
||||
display: 'none',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
'input[type=date]::-webkit-search-cancel-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
},
|
||||
'input[type="date"]::-webkit-clear-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
},
|
||||
|
||||
'input[type="date"]::-webkit-inner-spin-button,input[type="date"]::-webkit-outer-spin-button': {
|
||||
'-webkit-appearance': 'none',
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const DateComponent = ({ refFunction, classes, ...properties }) =>
|
||||
<MuiInput {...properties} ref={refFunction} type="date"/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentDate', DateComponent, [withStyles, styles]);
|
|
@ -0,0 +1,60 @@
|
|||
// Deprecated react-datetime version
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DateTimePicker from 'react-datetime';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
class DateComponent extends PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateDate = this.updateDate.bind(this);
|
||||
}
|
||||
|
||||
// when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
|
||||
// componentDidMount() {
|
||||
// if (this.props.value) {
|
||||
// this.updateDate(this.props.value);
|
||||
// }
|
||||
// }
|
||||
|
||||
updateDate(date) {
|
||||
this.context.updateCurrentValues({[this.props.path]: date});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const date = this.props.value ? (typeof this.props.value === 'string' ? new Date(this.props.value) : this.props.value) : null;
|
||||
|
||||
return (
|
||||
<div className="form-group row">
|
||||
<label className="control-label col-sm-3">{this.props.label}</label>
|
||||
<div className="col-sm-9">
|
||||
<DateTimePicker
|
||||
value={date}
|
||||
timeFormat={false}
|
||||
// newDate argument is a Moment object given by react-datetime
|
||||
onChange={newDate => this.updateDate(newDate)}
|
||||
inputProps={{name: this.props.name}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateComponent.propTypes = {
|
||||
control: PropTypes.any,
|
||||
datatype: PropTypes.any,
|
||||
group: PropTypes.any,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
DateComponent.contextTypes = {
|
||||
updateCurrentValues: PropTypes.func,
|
||||
};
|
||||
|
||||
export default DateComponent;
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
|
||||
|
||||
export const styles = theme => ({
|
||||
|
||||
'@global': {
|
||||
'input[type=datetime]::-ms-clear, input[type=datetime]::-ms-reveal': {
|
||||
display: 'none',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
'input[type=datetime]::-webkit-search-cancel-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
},
|
||||
'input[type="datetime"]::-webkit-clear-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
},
|
||||
|
||||
'input[type="datetime"]::-webkit-inner-spin-button,input[type="datetime"]::-webkit-outer-spin-button': {
|
||||
'-webkit-appearance': 'none',
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const DateTimeComponent = ({ refFunction, classes, ...properties }) =>
|
||||
<MuiInput {...properties} ref={refFunction} type="datetime-local"/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentDateTime', DateTimeComponent, [withStyles, styles]);
|
|
@ -0,0 +1,61 @@
|
|||
// Deprecated react-datetime version
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DateTimePicker from 'react-datetime';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
class DateTimeRdt extends PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateDate = this.updateDate.bind(this);
|
||||
}
|
||||
|
||||
// when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
|
||||
componentDidMount() {
|
||||
if (this.props.value) {
|
||||
this.updateDate(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
updateDate(date) {
|
||||
this.context.updateCurrentValues({[this.props.name]: date});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const date = this.props.value ? (typeof this.props.value === 'string' ? new Date(this.props.value) : this.props.value) : null;
|
||||
|
||||
return (
|
||||
<div className="form-group row">
|
||||
<label className="control-label col-sm-3">{this.props.label}</label>
|
||||
<div className="col-sm-9">
|
||||
<DateTimePicker
|
||||
value={date}
|
||||
// newDate argument is a Moment object given by react-datetime
|
||||
onChange={newDate => this.updateDate(newDate._d)}
|
||||
format={"x"}
|
||||
inputProps={{name: this.props.name}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTimeRdt.propTypes = {
|
||||
control: PropTypes.any,
|
||||
datatype: PropTypes.any,
|
||||
group: PropTypes.any,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
DateTimeRdt.contextTypes = {
|
||||
updateCurrentValues: PropTypes.func,
|
||||
};
|
||||
|
||||
export default DateTimeRdt;
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const Default = ({ refFunction, ...properties }) =>
|
||||
<MuiInput {...properties} ref={refFunction}/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentDefault', Default);
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const EmailComponent = ({ refFunction, ...properties }) =>
|
||||
<MuiInput {...properties} ref={refFunction} type="email" />;
|
||||
|
||||
|
||||
registerComponent('FormComponentEmail', EmailComponent);
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const NumberComponent = ({ refFunction, ...properties }) =>
|
||||
<MuiInput {...properties} ref={refFunction} type="number" />;
|
||||
|
||||
|
||||
registerComponent('FormComponentNumber', NumberComponent);
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import { getCountryInfo } from './RegionSelect';
|
||||
|
||||
|
||||
const PostalCode = ({ classes, refFunction, ...properties }) => {
|
||||
const currentCountryInfo = getCountryInfo(properties);
|
||||
const postalLabel = currentCountryInfo ? currentCountryInfo.postalLabel : 'Postal code';
|
||||
|
||||
return <MuiInput {...properties} ref={refFunction} label={postalLabel}/>;
|
||||
};
|
||||
|
||||
|
||||
registerComponent('PostalCode', PostalCode);
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import MuiRadioGroup from '../base-controls/MuiRadioGroup';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const RadioGroupComponent = ({ refFunction, ...properties }) =>
|
||||
<MuiRadioGroup {...properties} ref={refFunction}/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentRadioGroup', RadioGroupComponent);
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import MuiSuggest from '../base-controls/MuiSuggest';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import { countryInfo } from './countries';
|
||||
import _get from 'lodash/get';
|
||||
|
||||
|
||||
export const getCountryInfo = function (formComponentProps) {
|
||||
const addressPath = formComponentProps.path;
|
||||
const countryParts = addressPath.split('.');
|
||||
countryParts[countryParts.length-1] = 'country';
|
||||
const country = _get(formComponentProps.document, countryParts);
|
||||
return country && countryInfo[country];
|
||||
};
|
||||
|
||||
|
||||
const RegionSelect = ({ classes, refFunction, ...properties }) => {
|
||||
const currentCountryInfo = getCountryInfo(properties);
|
||||
const options = currentCountryInfo ? currentCountryInfo.regions : null;
|
||||
const regionLabel = currentCountryInfo ? currentCountryInfo.regionLabel : 'Region';
|
||||
|
||||
if (options) {
|
||||
return <MuiSuggest {...properties} ref={refFunction} options={options} label={regionLabel}/>;
|
||||
} else {
|
||||
return <MuiInput {...properties} ref={refFunction} label={regionLabel}/>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
registerComponent('RegionSelect', RegionSelect);
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import MuiSelect from '../base-controls/MuiSelect';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const SelectComponent = ({ refFunction, ...properties }) => {
|
||||
const noneOption = { label: '', value: '' };
|
||||
properties.options = [noneOption, ...properties.options];
|
||||
|
||||
return <MuiSelect {...properties} ref={refFunction}/>;
|
||||
};
|
||||
|
||||
|
||||
registerComponent('FormComponentSelect', SelectComponent);
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import MuiSelect from '../base-controls/MuiSelect';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const SelectMultiple = ({ refFunction, ...properties }) => {
|
||||
properties.multiple = true;
|
||||
|
||||
return <MuiSelect {...properties} ref={refFunction}/>;
|
||||
};
|
||||
|
||||
|
||||
registerComponent('FormComponentSelectMultiple', SelectMultiple);
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const TextareaComponent = ({ refFunction, ...properties }) =>
|
||||
<MuiInput {...properties}
|
||||
ref={refFunction}
|
||||
multiline={true}
|
||||
rows={properties.rows ? properties.rows : 2}
|
||||
rowsMax={10}
|
||||
/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentTextarea', TextareaComponent);
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
|
||||
|
||||
export const styles = theme => ({
|
||||
|
||||
'@global': {
|
||||
'input[type=time]::-ms-clear, input[type=time]::-ms-reveal': {
|
||||
display: 'none',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
'input[type=time]::-webkit-search-cancel-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
},
|
||||
'input[type="time"]::-webkit-clear-button': {
|
||||
display: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
},
|
||||
|
||||
'input[type="time"]::-webkit-inner-spin-button,input[type="time"]::-webkit-outer-spin-button': {
|
||||
'-webkit-appearance': 'none',
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
const TimeComponent = ({ refFunction, classes, ...properties }) =>
|
||||
<MuiInput {...properties} ref={refFunction} type="time"/>;
|
||||
|
||||
|
||||
registerComponent('FormComponentTime', TimeComponent, [withStyles, styles]);
|
|
@ -0,0 +1,73 @@
|
|||
// Deprecated react-datetime version
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DateTimePicker from 'react-datetime';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
class TimeRdt extends PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateDate = this.updateDate.bind(this);
|
||||
}
|
||||
|
||||
// when the datetime picker has mounted, SmartForm will catch the date value (no formsy mixin in this component)
|
||||
// componentDidMount() {
|
||||
// if (this.props.value) {
|
||||
// this.context.updateCurrentValues({[this.props.path]: this.props.value});
|
||||
// }
|
||||
// }
|
||||
|
||||
updateDate(mDate) {
|
||||
// if this is a properly formatted moment date, update time
|
||||
if (typeof mDate === 'object') {
|
||||
this.context.updateCurrentValues({[this.props.path]: mDate.format('HH:mm')});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const date = new Date();
|
||||
|
||||
// transform time string into date object to work inside datetimepicker
|
||||
const time = this.props.value;
|
||||
if (time) {
|
||||
date.setHours(parseInt(time.substr(0,2)), parseInt(time.substr(3,5)));
|
||||
} else {
|
||||
date.setHours(0,0);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="form-group row">
|
||||
<label className="control-label col-sm-3">{this.props.label}</label>
|
||||
<div className="col-sm-9">
|
||||
<DateTimePicker
|
||||
value={date}
|
||||
viewMode="time"
|
||||
dateFormat={false}
|
||||
timeFormat="HH:mm"
|
||||
// newDate argument is a Moment object given by react-datetime
|
||||
onChange={newDate => this.updateDate(newDate)}
|
||||
inputProps={{name: this.props.name}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TimeRdt.propTypes = {
|
||||
control: PropTypes.any,
|
||||
datatype: PropTypes.any,
|
||||
group: PropTypes.any,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
TimeRdt.contextTypes = {
|
||||
updateCurrentValues: PropTypes.func,
|
||||
};
|
||||
|
||||
export default TimeRdt;
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import MuiInput from '../base-controls/MuiInput';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
const UrlComponent = ({ refFunction, ...properties }) =>
|
||||
<MuiInput {...properties} ref={refFunction} type="url" />;
|
||||
|
||||
|
||||
registerComponent('FormComponentUrl', UrlComponent);
|
|
@ -0,0 +1,402 @@
|
|||
export const countries = [
|
||||
{ value: 'AF', label: 'Afghanistan' },
|
||||
{ value: 'AX', label: 'Åland Islands' },
|
||||
{ value: 'AL', label: 'Albania' },
|
||||
{ value: 'DZ', label: 'Algeria' },
|
||||
{ value: 'AS', label: 'American Samoa' },
|
||||
{ value: 'AD', label: 'Andorra' },
|
||||
{ value: 'AO', label: 'Angola' },
|
||||
{ value: 'AI', label: 'Anguilla' },
|
||||
{ value: 'AQ', label: 'Antarctica' },
|
||||
{ value: 'AG', label: 'Antigua and Barbuda' },
|
||||
{ value: 'AR', label: 'Argentina' },
|
||||
{ value: 'AM', label: 'Armenia' },
|
||||
{ value: 'AW', label: 'Aruba' },
|
||||
{ value: 'AU', label: 'Australia' },
|
||||
{ value: 'AT', label: 'Austria' },
|
||||
{ value: 'AZ', label: 'Azerbaijan' },
|
||||
{ value: 'BS', label: 'Bahamas' },
|
||||
{ value: 'BH', label: 'Bahrain' },
|
||||
{ value: 'BD', label: 'Bangladesh' },
|
||||
{ value: 'BB', label: 'Barbados' },
|
||||
{ value: 'BY', label: 'Belarus' },
|
||||
{ value: 'BE', label: 'Belgium' },
|
||||
{ value: 'BZ', label: 'Belize' },
|
||||
{ value: 'BJ', label: 'Benin' },
|
||||
{ value: 'BM', label: 'Bermuda' },
|
||||
{ value: 'BT', label: 'Bhutan' },
|
||||
{ value: 'BO', label: 'Bolivia' },
|
||||
{ value: 'BQ', label: 'Bonaire, Sint Eustatius, Saba' },
|
||||
{ value: 'BA', label: 'Bosnia and Herzegovina' },
|
||||
{ value: 'BW', label: 'Botswana' },
|
||||
{ value: 'BV', label: 'Bouvet Island' },
|
||||
{ value: 'BR', label: 'Brazil' },
|
||||
{ value: 'IO', label: 'British Indian Ocean Territory' },
|
||||
{ value: 'BN', label: 'Brunei Darussalam' },
|
||||
{ value: 'BG', label: 'Bulgaria' },
|
||||
{ value: 'BF', label: 'Burkina Faso' },
|
||||
{ value: 'BI', label: 'Burundi' },
|
||||
{ value: 'CV', label: 'Cabo Verde' },
|
||||
{ value: 'KH', label: 'Cambodia' },
|
||||
{ value: 'CM', label: 'Cameroon' },
|
||||
{ value: 'CA', label: 'Canada' },
|
||||
{ value: 'KY', label: 'Cayman Islands' },
|
||||
{ value: 'CF', label: 'Central African Republic' },
|
||||
{ value: 'TD', label: 'Chad' },
|
||||
{ value: 'CL', label: 'Chile' },
|
||||
{ value: 'CN', label: 'China' },
|
||||
{ value: 'CX', label: 'Christmas Island' },
|
||||
{ value: 'CC', label: 'Cocos (Keeling) Islands' },
|
||||
{ value: 'CO', label: 'Colombia' },
|
||||
{ value: 'KM', label: 'Comoros' },
|
||||
{ value: 'CG', label: 'Congo' },
|
||||
{ value: 'CD', label: 'Congo (Democratic Republic of the)' },
|
||||
{ value: 'CK', label: 'Cook Islands' },
|
||||
{ value: 'CR', label: 'Costa Rica' },
|
||||
{ value: 'CI', label: 'Côte d’Ivoire' },
|
||||
{ value: 'HR', label: 'Croatia' },
|
||||
{ value: 'CU', label: 'Cuba' },
|
||||
{ value: 'CW', label: 'Curaçao' },
|
||||
{ value: 'CY', label: 'Cyprus' },
|
||||
{ value: 'CZ', label: 'Czechia' },
|
||||
{ value: 'DK', label: 'Denmark' },
|
||||
{ value: 'DJ', label: 'Djibouti' },
|
||||
{ value: 'DM', label: 'Dominica' },
|
||||
{ value: 'DO', label: 'Dominican Republic' },
|
||||
{ value: 'EC', label: 'Ecuador' },
|
||||
{ value: 'EG', label: 'Egypt' },
|
||||
{ value: 'SV', label: 'El Salvador' },
|
||||
{ value: 'GQ', label: 'Equatorial Guinea' },
|
||||
{ value: 'ER', label: 'Eritrea' },
|
||||
{ value: 'EE', label: 'Estonia' },
|
||||
{ value: 'ET', label: 'Ethiopia' },
|
||||
{ value: 'FK', label: 'Falkland Islands' },
|
||||
{ value: 'FO', label: 'Faroe Islands' },
|
||||
{ value: 'FJ', label: 'Fiji' },
|
||||
{ value: 'FI', label: 'Finland' },
|
||||
{ value: 'FR', label: 'France' },
|
||||
{ value: 'GF', label: 'French Guiana' },
|
||||
{ value: 'PF', label: 'French Polynesia' },
|
||||
{ value: 'TF', label: 'French Southern Territories' },
|
||||
{ value: 'GA', label: 'Gabon' },
|
||||
{ value: 'GM', label: 'Gambia' },
|
||||
{ value: 'GE', label: 'Georgia' },
|
||||
{ value: 'DE', label: 'Germany' },
|
||||
{ value: 'GH', label: 'Ghana' },
|
||||
{ value: 'GI', label: 'Gibraltar' },
|
||||
{ value: 'GR', label: 'Greece' },
|
||||
{ value: 'GL', label: 'Greenland' },
|
||||
{ value: 'GD', label: 'Grenada' },
|
||||
{ value: 'GP', label: 'Guadeloupe' },
|
||||
{ value: 'GU', label: 'Guam' },
|
||||
{ value: 'GT', label: 'Guatemala' },
|
||||
{ value: 'GG', label: 'Guernsey' },
|
||||
{ value: 'GN', label: 'Guinea' },
|
||||
{ value: 'GW', label: 'Guinea-Bissau' },
|
||||
{ value: 'GY', label: 'Guyana' },
|
||||
{ value: 'HT', label: 'Haiti' },
|
||||
{ value: 'HM', label: 'Heard Island, Mcdonald Islands' },
|
||||
{ value: 'VA', label: 'Vatican City State' },
|
||||
{ value: 'HN', label: 'Honduras' },
|
||||
{ value: 'HK', label: 'Hong Kong' },
|
||||
{ value: 'HU', label: 'Hungary' },
|
||||
{ value: 'IS', label: 'Iceland' },
|
||||
{ value: 'IN', label: 'India' },
|
||||
{ value: 'ID', label: 'Indonesia' },
|
||||
{ value: 'IR', label: 'Iran' },
|
||||
{ value: 'IQ', label: 'Iraq' },
|
||||
{ value: 'IE', label: 'Ireland' },
|
||||
{ value: 'IM', label: 'Isle of Man' },
|
||||
{ value: 'IL', label: 'Israel' },
|
||||
{ value: 'IT', label: 'Italy' },
|
||||
{ value: 'JM', label: 'Jamaica' },
|
||||
{ value: 'JP', label: 'Japan' },
|
||||
{ value: 'JE', label: 'Jersey' },
|
||||
{ value: 'JO', label: 'Jordan' },
|
||||
{ value: 'KZ', label: 'Kazakhstan' },
|
||||
{ value: 'KE', label: 'Kenya' },
|
||||
{ value: 'KI', label: 'Kiribati' },
|
||||
{ value: 'KW', label: 'Kuwait' },
|
||||
{ value: 'KG', label: 'Kyrgyzstan' },
|
||||
{ value: 'LA', label: 'Lao' },
|
||||
{ value: 'LV', label: 'Latvia' },
|
||||
{ value: 'LB', label: 'Lebanon' },
|
||||
{ value: 'LS', label: 'Lesotho' },
|
||||
{ value: 'LR', label: 'Liberia' },
|
||||
{ value: 'LY', label: 'Libya' },
|
||||
{ value: 'LI', label: 'Liechtenstein' },
|
||||
{ value: 'LT', label: 'Lithuania' },
|
||||
{ value: 'LU', label: 'Luxembourg' },
|
||||
{ value: 'MO', label: 'Macao' },
|
||||
{ value: 'MK', label: 'Macedonia' },
|
||||
{ value: 'MG', label: 'Madagascar' },
|
||||
{ value: 'MW', label: 'Malawi' },
|
||||
{ value: 'MY', label: 'Malaysia' },
|
||||
{ value: 'MV', label: 'Maldives' },
|
||||
{ value: 'ML', label: 'Mali' },
|
||||
{ value: 'MT', label: 'Malta' },
|
||||
{ value: 'MH', label: 'Marshall Islands' },
|
||||
{ value: 'MQ', label: 'Martinique' },
|
||||
{ value: 'MR', label: 'Mauritania' },
|
||||
{ value: 'MU', label: 'Mauritius' },
|
||||
{ value: 'YT', label: 'Mayotte' },
|
||||
{ value: 'MX', label: 'Mexico' },
|
||||
{ value: 'FM', label: 'Micronesia' },
|
||||
{ value: 'MD', label: 'Moldova' },
|
||||
{ value: 'MC', label: 'Monaco' },
|
||||
{ value: 'MN', label: 'Mongolia' },
|
||||
{ value: 'ME', label: 'Montenegro' },
|
||||
{ value: 'MS', label: 'Montserrat' },
|
||||
{ value: 'MA', label: 'Morocco' },
|
||||
{ value: 'MZ', label: 'Mozambique' },
|
||||
{ value: 'MM', label: 'Myanmar' },
|
||||
{ value: 'NA', label: 'Namibia' },
|
||||
{ value: 'NR', label: 'Nauru' },
|
||||
{ value: 'NP', label: 'Nepal' },
|
||||
{ value: 'NL', label: 'Netherlands' },
|
||||
{ value: 'NC', label: 'New Caledonia' },
|
||||
{ value: 'NZ', label: 'New Zealand' },
|
||||
{ value: 'NI', label: 'Nicaragua' },
|
||||
{ value: 'NE', label: 'Niger' },
|
||||
{ value: 'NG', label: 'Nigeria' },
|
||||
{ value: 'NU', label: 'Niue' },
|
||||
{ value: 'NF', label: 'Norfolk Island' },
|
||||
{ value: 'KP', label: 'North Korea' },
|
||||
{ value: 'MP', label: 'Northern Mariana Islands' },
|
||||
{ value: 'NO', label: 'Norway' },
|
||||
{ value: 'OM', label: 'Oman' },
|
||||
{ value: 'PK', label: 'Pakistan' },
|
||||
{ value: 'PW', label: 'Palau' },
|
||||
{ value: 'PS', label: 'Palestine' },
|
||||
{ value: 'PA', label: 'Panama' },
|
||||
{ value: 'PG', label: 'Papua New Guinea' },
|
||||
{ value: 'PY', label: 'Paraguay' },
|
||||
{ value: 'PE', label: 'Peru' },
|
||||
{ value: 'PH', label: 'Philippines' },
|
||||
{ value: 'PN', label: 'Pitcairn' },
|
||||
{ value: 'PL', label: 'Poland' },
|
||||
{ value: 'PT', label: 'Portugal' },
|
||||
{ value: 'PR', label: 'Puerto Rico' },
|
||||
{ value: 'QA', label: 'Qatar' },
|
||||
{ value: 'RE', label: 'Réunion' },
|
||||
{ value: 'RO', label: 'Romania' },
|
||||
{ value: 'RU', label: 'Russian Federation' },
|
||||
{ value: 'RW', label: 'Rwanda' },
|
||||
{ value: 'BL', label: 'Saint Barthélemy' },
|
||||
{ value: 'SH', label: 'Saint Helena, Ascension, Tristan Da Cunha' },
|
||||
{ value: 'KN', label: 'Saint Kitts and Nevis' },
|
||||
{ value: 'LC', label: 'Saint Lucia' },
|
||||
{ value: 'MF', label: 'Saint Martin (French Portion)' },
|
||||
{ value: 'PM', label: 'Saint Pierre and Miquelon' },
|
||||
{ value: 'VC', label: 'Saint Vincent and the Grenadines' },
|
||||
{ value: 'WS', label: 'Samoa' },
|
||||
{ value: 'SM', label: 'San Marino' },
|
||||
{ value: 'ST', label: 'Sao Tome and Principe' },
|
||||
{ value: 'SA', label: 'Saudi Arabia' },
|
||||
{ value: 'SN', label: 'Senegal' },
|
||||
{ value: 'RS', label: 'Serbia' },
|
||||
{ value: 'SC', label: 'Seychelles' },
|
||||
{ value: 'SL', label: 'Sierra Leone' },
|
||||
{ value: 'SG', label: 'Singapore' },
|
||||
{ value: 'SX', label: 'Sint Maarten (Dutch part)' },
|
||||
{ value: 'SK', label: 'Slovakia' },
|
||||
{ value: 'SI', label: 'Slovenia' },
|
||||
{ value: 'SB', label: 'Solomon Islands' },
|
||||
{ value: 'SO', label: 'Somalia' },
|
||||
{ value: 'ZA', label: 'South Africa' },
|
||||
{ value: 'GS', label: 'South Georgia, South Sandwich Islands' },
|
||||
{ value: 'KR', label: 'South Korea' },
|
||||
{ value: 'SS', label: 'South Sudan' },
|
||||
{ value: 'ES', label: 'Spain' },
|
||||
{ value: 'LK', label: 'Sri Lanka' },
|
||||
{ value: 'SD', label: 'Sudan' },
|
||||
{ value: 'SR', label: 'Suriname' },
|
||||
{ value: 'SJ', label: 'Svalbard and Jan Mayen' },
|
||||
{ value: 'SZ', label: 'Swaziland' },
|
||||
{ value: 'SE', label: 'Sweden' },
|
||||
{ value: 'CH', label: 'Switzerland' },
|
||||
{ value: 'SY', label: 'Syria' },
|
||||
{ value: 'TW', label: 'Taiwan' },
|
||||
{ value: 'TJ', label: 'Tajikistan' },
|
||||
{ value: 'TZ', label: 'Tanzania' },
|
||||
{ value: 'TH', label: 'Thailand' },
|
||||
{ value: 'TL', label: 'Timor-Leste' },
|
||||
{ value: 'TG', label: 'Togo' },
|
||||
{ value: 'TK', label: 'Tokelau' },
|
||||
{ value: 'TO', label: 'Tonga' },
|
||||
{ value: 'TT', label: 'Trinidad and Tobago' },
|
||||
{ value: 'TN', label: 'Tunisia' },
|
||||
{ value: 'TR', label: 'Turkey' },
|
||||
{ value: 'TM', label: 'Turkmenistan' },
|
||||
{ value: 'TC', label: 'Turks and Caicos Islands' },
|
||||
{ value: 'TV', label: 'Tuvalu' },
|
||||
{ value: 'UG', label: 'Uganda' },
|
||||
{ value: 'UA', label: 'Ukraine' },
|
||||
{ value: 'AE', label: 'United Arab Emirates' },
|
||||
{ value: 'GB', label: 'United Kingdom' },
|
||||
{ value: 'US', label: 'United States' },
|
||||
{ value: 'UM', label: 'United States Minor Outlying Islands' },
|
||||
{ value: 'UY', label: 'Uruguay' },
|
||||
{ value: 'UZ', label: 'Uzbekistan' },
|
||||
{ value: 'VU', label: 'Vanuatu' },
|
||||
{ value: 'VE', label: 'Venezuela' },
|
||||
{ value: 'VN', label: 'Viet Nam' },
|
||||
{ value: 'VG', label: 'Virgin Islands, British' },
|
||||
{ value: 'VI', label: 'Virgin Islands, U.S.' },
|
||||
{ value: 'WF', label: 'Wallis and Futuna' },
|
||||
{ value: 'EH', label: 'Western Sahara' },
|
||||
{ value: 'YE', label: 'Yemen' },
|
||||
{ value: 'ZM', label: 'Zambia' },
|
||||
{ value: 'ZW', label: 'Zimbabwe' },
|
||||
];
|
||||
|
||||
|
||||
export const countryInfo = {
|
||||
US: {
|
||||
regionLabel: 'State',
|
||||
postalLabel: 'Zip code',
|
||||
regions: [
|
||||
{ value: 'AL', label: 'Alabama' },
|
||||
{ value: 'AK', label: 'Alaska' },
|
||||
{ value: 'AZ', label: 'Arizona' },
|
||||
{ value: 'AR', label: 'Arkansas' },
|
||||
{ value: 'CA', label: 'California' },
|
||||
{ value: 'CO', label: 'Colorado' },
|
||||
{ value: 'CT', label: 'Connecticut' },
|
||||
{ value: 'DE', label: 'Delaware' },
|
||||
{ value: 'FL', label: 'Florida' },
|
||||
{ value: 'GA', label: 'Georgia' },
|
||||
{ value: 'HI', label: 'Hawaii' },
|
||||
{ value: 'ID', label: 'Idaho' },
|
||||
{ value: 'IL', label: 'Illinois' },
|
||||
{ value: 'IN', label: 'Indiana' },
|
||||
{ value: 'IA', label: 'Iowa' },
|
||||
{ value: 'KS', label: 'Kansas' },
|
||||
{ value: 'KY', label: 'Kentucky' },
|
||||
{ value: 'LA', label: 'Louisiana' },
|
||||
{ value: 'ME', label: 'Maine' },
|
||||
{ value: 'MD', label: 'Maryland' },
|
||||
{ value: 'MA', label: 'Massachusetts' },
|
||||
{ value: 'MI', label: 'Michigan' },
|
||||
{ value: 'MN', label: 'Minnesota' },
|
||||
{ value: 'MS', label: 'Mississippi' },
|
||||
{ value: 'MO', label: 'Missouri' },
|
||||
{ value: 'MT', label: 'Montana' },
|
||||
{ value: 'NE', label: 'Nebraska' },
|
||||
{ value: 'NV', label: 'Nevada' },
|
||||
{ value: 'NH', label: 'New Hampshire' },
|
||||
{ value: 'NJ', label: 'New Jersey' },
|
||||
{ value: 'NM', label: 'New Mexico' },
|
||||
{ value: 'NY', label: 'New York' },
|
||||
{ value: 'NC', label: 'North Carolina' },
|
||||
{ value: 'ND', label: 'North Dakota' },
|
||||
{ value: 'OH', label: 'Ohio' },
|
||||
{ value: 'OK', label: 'Oklahoma' },
|
||||
{ value: 'OR', label: 'Oregon' },
|
||||
{ value: 'PA', label: 'Pennsylvania' },
|
||||
{ value: 'RI', label: 'Rhode Island' },
|
||||
{ value: 'SC', label: 'South Carolina' },
|
||||
{ value: 'SD', label: 'South Dakota' },
|
||||
{ value: 'TN', label: 'Tennessee' },
|
||||
{ value: 'TX', label: 'Texas' },
|
||||
{ value: 'UT', label: 'Utah' },
|
||||
{ value: 'VT', label: 'Vermont' },
|
||||
{ value: 'VA', label: 'Virginia' },
|
||||
{ value: 'WA', label: 'Washington' },
|
||||
{ value: 'WV', label: 'West Virginia' },
|
||||
{ value: 'WI', label: 'Wisconsin' },
|
||||
{ value: 'WY', label: 'Wyoming' },
|
||||
]
|
||||
},
|
||||
CA: {
|
||||
regionLabel: 'Province',
|
||||
postalLabel: 'Postal code',
|
||||
regions: [
|
||||
{ value: 'AB', label: 'Alberta' },
|
||||
{ value: 'BC', label: 'British Columbia' },
|
||||
{ value: 'MB', label: 'Manitoba' },
|
||||
{ value: 'NB', label: 'New Brunswick' },
|
||||
{ value: 'NL', label: 'Newfoundland and Labrador' },
|
||||
{ value: 'NS', label: 'Nova Scotia' },
|
||||
{ value: 'NT', label: 'Northwest Territories' },
|
||||
{ value: 'NU', label: 'Nunavut' },
|
||||
{ value: 'ON', label: 'Ontario' },
|
||||
{ value: 'PE', label: 'Prince Edward Island' },
|
||||
{ value: 'QC', label: 'Quebec' },
|
||||
{ value: 'SK', label: 'Saskatchewan' },
|
||||
{ value: 'YT', label: 'Yukon' },
|
||||
],
|
||||
},
|
||||
AU: {
|
||||
regionLabel: 'State',
|
||||
postalLabel: 'Postcode',
|
||||
regions: [
|
||||
{ value: 'ACT', label: 'Australian Capital Territory' },
|
||||
{ value: 'NSW', label: 'New South Wales' },
|
||||
{ value: 'NT', label: 'Northern Territory' },
|
||||
{ value: 'QLD', label: 'Queensland' },
|
||||
{ value: 'SA', label: 'South Australia' },
|
||||
{ value: 'TAS', label: 'Tasmania' },
|
||||
{ value: 'VIC', label: 'Victoria' },
|
||||
{ value: 'WA', label: 'Western Australia' },
|
||||
],
|
||||
},
|
||||
UK: {
|
||||
regionLabel: 'County',
|
||||
postalLabel: 'Postcode',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const getCountryLabel = (countryValue) => {
|
||||
const country = countries.find(country => country.value === countryValue);
|
||||
return country ? country.label : '';
|
||||
};
|
||||
|
||||
|
||||
export const getCountryContinent = (countryValue) => {
|
||||
const country = countries.find(country => country.value === countryValue);
|
||||
return country ? country.continent : '';
|
||||
};
|
||||
|
||||
|
||||
export const getRegionLabel = (countryValue, regionValue) => {
|
||||
if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {
|
||||
return regionValue;
|
||||
}
|
||||
|
||||
const regions = countryInfo[countryValue].regions;
|
||||
|
||||
let region = regions.find(nextRegion => nextRegion.value === regionValue);
|
||||
|
||||
if (region) {
|
||||
return region.label;
|
||||
} else {
|
||||
return regionValue;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Given a region value or label, returns the region value (QC or Quebec => QC)
|
||||
// or false if the regionValue is invalid
|
||||
export const validateRegion = (countryValue, regionValue) => {
|
||||
if (!countryInfo[countryValue] || !countryInfo[countryValue].regions) {
|
||||
return regionValue;
|
||||
}
|
||||
|
||||
const regions = countryInfo[countryValue].regions;
|
||||
|
||||
let region = regions.find(nextRegion => nextRegion.value === regionValue);
|
||||
|
||||
if (region) {
|
||||
return regionValue;
|
||||
}
|
||||
|
||||
region = regions.find(nextRegion => nextRegion.label === regionValue);
|
||||
|
||||
if (region) {
|
||||
return region.value;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
58
packages/vulcan-ui-material/components/index.js
Normal file
58
packages/vulcan-ui-material/components/index.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import './accounts/AccountsButton';
|
||||
import './accounts/AccountsButtons';
|
||||
import './accounts/AccountsField';
|
||||
import './accounts/AccountsFields';
|
||||
import './accounts/AccountsForm';
|
||||
import './accounts/AccountsPasswordOrService';
|
||||
import './accounts/AccountsSocialButtons';
|
||||
|
||||
import './bonus/LoadMore';
|
||||
import './bonus/SearchInput';
|
||||
import './bonus/TooltipIntl';
|
||||
import './bonus/TooltipIconButton';
|
||||
|
||||
import './core/Card';
|
||||
import './core/Datatable';
|
||||
import './core/EditButton';
|
||||
import './core/Loading';
|
||||
import './core/ModalTrigger';
|
||||
import './core/NewButton';
|
||||
|
||||
import './forms/FormComponentInner';
|
||||
import './forms/FormErrors';
|
||||
import './forms/FormGroup';
|
||||
import './forms/FormGroupNone';
|
||||
import './forms/FormGroupWithLine';
|
||||
import './forms/FormNested';
|
||||
import './forms/FormNestedDivider';
|
||||
import './forms/FormNestedFoot';
|
||||
import './forms/FormNestedHead';
|
||||
import './forms/FormSubmit';
|
||||
import './forms/controls/Checkbox';
|
||||
import './forms/controls/CheckboxGroup';
|
||||
import './forms/controls/CountrySelect';
|
||||
import './forms/controls/Date';
|
||||
import './forms/controls/DateRdt';
|
||||
import './forms/controls/DateTime';
|
||||
import './forms/controls/DateTimeRdt';
|
||||
import './forms/controls/Default';
|
||||
import './forms/controls/Email';
|
||||
import './forms/controls/Number';
|
||||
import './forms/controls/PostalCode';
|
||||
import './forms/controls/RadioGroup';
|
||||
import './forms/controls/RegionSelect';
|
||||
import './forms/controls/Select';
|
||||
import './forms/controls/Textarea';
|
||||
import './forms/controls/Time';
|
||||
import './forms/controls/TimeRdt';
|
||||
import './forms/controls/Url';
|
||||
|
||||
import './theme/ThemeStyles';
|
||||
|
||||
import './ui/Button';
|
||||
import './ui/Alert';
|
||||
|
||||
import './upload/UploadImage';
|
||||
import './upload/UploadInner';
|
||||
|
||||
export * from './forms/controls/countries';
|
23
packages/vulcan-ui-material/components/theme/JssCleanup.jsx
Normal file
23
packages/vulcan-ui-material/components/theme/JssCleanup.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
|
||||
|
||||
class JssCleanup extends PureComponent {
|
||||
|
||||
|
||||
// Remove the server-side injected CSS.
|
||||
componentDidMount() {
|
||||
if (!document || !document.getElementById) return;
|
||||
|
||||
const jssStyles = document.getElementById('jss-server-side');
|
||||
if (jssStyles && jssStyles.parentNode) {
|
||||
// jssStyles.parentNode.removeChild(jssStyles);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default JssCleanup;
|
232
packages/vulcan-ui-material/components/theme/ThemeStyles.jsx
Normal file
232
packages/vulcan-ui-material/components/theme/ThemeStyles.jsx
Normal file
|
@ -0,0 +1,232 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import withTheme from '@material-ui/core/styles/withTheme';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { getContrastRatio } from '@material-ui/core/styles/colorManipulator';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const describeTypography = (theme, className) => {
|
||||
const typography = className ? theme.typography[className] : theme.typography;
|
||||
const fontFamily = typography.fontFamily.split(',')[0];
|
||||
return `${fontFamily} ${typography.fontWeight} ${typography.fontSize}px`;
|
||||
};
|
||||
|
||||
|
||||
const mainPalette = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
const altPalette = ['A100', 'A200', 'A400', 'A700'];
|
||||
|
||||
|
||||
function getColorBlock(theme, classes, colorName, colorValue, colorTitle) {
|
||||
const bgColor = theme.palette[colorName][colorValue];
|
||||
|
||||
let fgColor = theme.palette.common.black;
|
||||
if (getContrastRatio(bgColor, fgColor) < 7) {
|
||||
fgColor = theme.palette.common.white;
|
||||
}
|
||||
|
||||
let blockTitle;
|
||||
if (colorTitle) {
|
||||
blockTitle = <div className={classes.name}>{colorName}</div>;
|
||||
}
|
||||
|
||||
let rowStyle = {
|
||||
backgroundColor: bgColor,
|
||||
color: fgColor,
|
||||
listStyle: 'none',
|
||||
padding: 15,
|
||||
};
|
||||
|
||||
if (colorValue.toString().indexOf('A1') === 0) {
|
||||
rowStyle = {
|
||||
...rowStyle,
|
||||
marginTop: 4,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<li style={rowStyle} key={colorValue}>
|
||||
{blockTitle}
|
||||
<div className={classes.colorContainer}>
|
||||
<span>{colorValue}</span>
|
||||
<span className={classes.colorValue}>{bgColor.toUpperCase()}</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function getColorGroup(options) {
|
||||
const { theme, classes, color, showAltPalette } = options;
|
||||
const cssColor = color.replace(' ', '').replace(color.charAt(0), color.charAt(0).toLowerCase());
|
||||
let colorsList = [];
|
||||
colorsList = mainPalette.map(mainValue => getColorBlock(theme, classes, cssColor, mainValue));
|
||||
|
||||
if (showAltPalette) {
|
||||
altPalette.forEach(altValue => {
|
||||
colorsList.push(getColorBlock(theme, classes, cssColor, altValue));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={classes.colorGroup} key={cssColor}>
|
||||
{getColorBlock(theme, classes, cssColor, 500, true)}
|
||||
<div className={classes.blockSpace} />
|
||||
{colorsList}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {},
|
||||
paper: {
|
||||
padding: theme.spacing.unit * 3,
|
||||
},
|
||||
name: {
|
||||
marginBottom: 60,
|
||||
},
|
||||
blockSpace: {
|
||||
height: 4,
|
||||
},
|
||||
colorContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
colorGroup: {
|
||||
padding: '16px 0',
|
||||
margin: '0 15px 0 0',
|
||||
flexGrow: 1,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
},
|
||||
colorValue: {
|
||||
...theme.typography.caption,
|
||||
color: 'inherit',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const latin = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur justo quam, ' +
|
||||
'pellentesque ultrices ex a, aliquet porttitor ante. Donec tellus arcu, viverra ut lorem id, ' +
|
||||
'ultrices ultricies enim. Donec enim metus, sollicitudin id lobortis id, iaculis ut arcu. ' +
|
||||
'Maecenas sollicitudin congue nisi. Donec convallis, ipsum ac ultricies dignissim, orci ex ' +
|
||||
'efficitur lectus, ac lacinia risus nunc at diam. Nam gravida bibendum lectus. Donec ' +
|
||||
'scelerisque sem nec urna vestibulum vehicula.';
|
||||
|
||||
|
||||
const ThemeStyles = ({ theme, classes }) => {
|
||||
return (
|
||||
<Grid container className={classNames('theme-styles', classes.root)}>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h1">
|
||||
h1: {describeTypography(theme, 'h1')}
|
||||
</Typography>
|
||||
<Typography variant="h2">
|
||||
h2: {describeTypography(theme, 'h2')}
|
||||
</Typography>
|
||||
<Typography variant="h3">
|
||||
h3: {describeTypography(theme, 'h3')}
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
h4: {describeTypography(theme, 'h4')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Headline: {describeTypography(theme, 'h5')}
|
||||
</Typography>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Title: {describeTypography(theme, 'h6')}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Subtitle1: {describeTypography(theme, 'subtitle1')}
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Body 1: {describeTypography(theme, 'body1')} - {latin}
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{latin}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Body 2: {describeTypography(theme, 'body2')} - {latin}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{latin}
|
||||
</Typography>
|
||||
<Typography variant="caption" gutterBottom align="center">
|
||||
Caption: {describeTypography(theme, 'caption')}
|
||||
</Typography>
|
||||
<Typography gutterBottom>
|
||||
Base: {describeTypography(theme)} - {latin}
|
||||
</Typography>
|
||||
<Typography variant="button" gutterBottom align="center">
|
||||
Button - {describeTypography(theme)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
{
|
||||
getColorGroup({
|
||||
theme,
|
||||
classes,
|
||||
color: 'primary',
|
||||
showAltPalette: true,
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
{
|
||||
getColorGroup({
|
||||
theme,
|
||||
classes,
|
||||
color: 'secondary',
|
||||
showAltPalette: true,
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
{
|
||||
getColorGroup({
|
||||
theme,
|
||||
classes,
|
||||
color: 'error',
|
||||
showAltPalette: true,
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
{
|
||||
getColorGroup({
|
||||
theme,
|
||||
classes,
|
||||
color: 'background',
|
||||
showAltPalette: true,
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
ThemeStyles.propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
registerComponent('ThemeStyles', ThemeStyles, [withTheme, null], [withStyles, styles]);
|
31
packages/vulcan-ui-material/components/ui/Alert.jsx
Normal file
31
packages/vulcan-ui-material/components/ui/Alert.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @Author: Apollinaire Lecocq <apollinaire>
|
||||
* @Date: 09-01-19
|
||||
* @Last modified by: apollinaire
|
||||
* @Last modified time: 10-01-19
|
||||
*/
|
||||
import React from 'react';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import { registerComponent } from 'meteor/vulcan:core';
|
||||
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
|
||||
const AlertStyle = theme => ({
|
||||
error: {
|
||||
color: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error[100],
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
},
|
||||
other: {
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
},
|
||||
});
|
||||
|
||||
const Alert = ({ children, variant, classes, ...rest }) => (
|
||||
<Card className={variant === 'danger' ? classes.error : classes.other}>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
registerComponent({ name: 'Alert', component: Alert, hocs: [[withStyles, AlertStyle]] });
|
51
packages/vulcan-ui-material/components/ui/Button.jsx
Normal file
51
packages/vulcan-ui-material/components/ui/Button.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import MuiButton from '@material-ui/core/Button';
|
||||
import MuiIconButton from '@material-ui/core/IconButton';
|
||||
|
||||
|
||||
const Button = ({ children, color, variant, size, iconButton, ...rest }) => {
|
||||
switch(variant) {
|
||||
case 'success':
|
||||
color = 'primary';
|
||||
variant = null;
|
||||
break;
|
||||
case 'danger':
|
||||
color = 'default';
|
||||
variant = null;
|
||||
break;
|
||||
case 'inverse':
|
||||
color = 'inherit';
|
||||
variant = null;
|
||||
break;
|
||||
}
|
||||
|
||||
switch(size) {
|
||||
case 'xsmall':
|
||||
size = 'small';
|
||||
break;
|
||||
case 'small':
|
||||
size = 'medium';
|
||||
break;
|
||||
case 'large':
|
||||
size = 'large';
|
||||
break;
|
||||
}
|
||||
|
||||
if (iconButton) {
|
||||
return (
|
||||
<MuiIconButton color={color} variant={variant} size={size} {...rest}>
|
||||
{children}
|
||||
</MuiIconButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiButton color={color} variant={variant} size={size} {...rest}>
|
||||
{children}
|
||||
</MuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
registerComponent('Button', Button);
|
117
packages/vulcan-ui-material/components/upload/UploadImage.jsx
Executable file
117
packages/vulcan-ui-material/components/upload/UploadImage.jsx
Executable file
|
@ -0,0 +1,117 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent } from 'meteor/vulcan:lib';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import DeleteIcon from 'mdi-material-ui/Delete';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Used by UploadInner to display a single image
|
||||
*/
|
||||
const styles = theme => ({
|
||||
|
||||
uploadImage: {
|
||||
textAlign: 'center',
|
||||
marginBottom: theme.spacing.unit * -1,
|
||||
marginLeft: theme.spacing.unit * 0.5,
|
||||
marginRight: theme.spacing.unit * 0.5,
|
||||
},
|
||||
|
||||
uploadImageContents: {
|
||||
position: 'relative',
|
||||
},
|
||||
|
||||
uploadImageImg: {
|
||||
display: 'block',
|
||||
maxWidth: 150,
|
||||
maxHeight: 150,
|
||||
},
|
||||
|
||||
uploadLoading: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: 'rgba(255,255,255,0.8)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
span: {
|
||||
display: 'block',
|
||||
fontSize: '1.5rem',
|
||||
}
|
||||
},
|
||||
|
||||
deleteButton: {
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
class UploadImage extends PureComponent {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.handleClear = this.handleClear.bind(this);
|
||||
}
|
||||
|
||||
handleClear (event) {
|
||||
event.preventDefault();
|
||||
this.props.clearImage(this.props.index);
|
||||
}
|
||||
|
||||
// Get the URL of an image or the first in an array of images
|
||||
getImageUrl (imageOrImageArray) {
|
||||
// if image is actually an array of formats, use first format
|
||||
const image = Array.isArray(imageOrImageArray) ? imageOrImageArray[0] : imageOrImageArray;
|
||||
|
||||
// if image is an object, return secure_url; else return image itself
|
||||
return typeof image === 'string' ? image : image.secure_url;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { loading, error, image, style, classes } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.uploadImage}>
|
||||
|
||||
<div className={classes.uploadImageContents}>
|
||||
|
||||
<img className={classes.uploadImageImg} src={this.getImageUrl(image)} style={style}/>
|
||||
{
|
||||
loading &&
|
||||
|
||||
<div className={classes.uploadLoading}>
|
||||
<Components.Loading/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<IconButton className={classes.deleteButton} onClick={this.handleClear}>
|
||||
<DeleteIcon/>
|
||||
</IconButton>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UploadImage.propTypes = {
|
||||
clearImage: PropTypes.func.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
image: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
style: PropTypes.object,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
UploadImage.displayName = 'UploadImageMui';
|
||||
|
||||
|
||||
registerComponent('UploadImage', UploadImage, [withStyles, styles]);
|
181
packages/vulcan-ui-material/components/upload/UploadInner.jsx
Executable file
181
packages/vulcan-ui-material/components/upload/UploadInner.jsx
Executable file
|
@ -0,0 +1,181 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent, getComponent } from 'meteor/vulcan:lib';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Material UI GUI for Cloudinary Image Upload component
|
||||
|
||||
*/
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
|
||||
root: {},
|
||||
|
||||
label: {},
|
||||
|
||||
uploadField: {
|
||||
marginTop: theme.spacing.unit,
|
||||
},
|
||||
|
||||
dropzoneBase: {
|
||||
borderWidth: 3,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: theme.palette.background[900],
|
||||
backgroundColor: theme.palette.background[100],
|
||||
color: theme.palette.common.lightBlack,
|
||||
padding: '30px 60px',
|
||||
transition: 'all 0.5s',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'&[aria-disabled="false"]:hover': {
|
||||
color: theme.palette.common.midBlack,
|
||||
borderColor: theme.palette.background['A200'],
|
||||
}
|
||||
},
|
||||
|
||||
dropzoneActive: {
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.status.info,
|
||||
},
|
||||
|
||||
dropzoneReject: {
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.palette.status.danger,
|
||||
},
|
||||
|
||||
uploadState: {},
|
||||
|
||||
uploadImages: {
|
||||
border: `1px solid ${theme.palette.background[500]}`,
|
||||
backgroundColor: theme.palette.background[100],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
paddingTop: theme.spacing.unit,
|
||||
paddingRight: theme.spacing.unit * 0.5,
|
||||
paddingBottom: theme.spacing.unit,
|
||||
paddingLeft: theme.spacing.unit * 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const UploadInner = (props) => {
|
||||
const {
|
||||
uploading,
|
||||
images,
|
||||
disabled,
|
||||
maxCount,
|
||||
label,
|
||||
help,
|
||||
options,
|
||||
enableMultiple,
|
||||
onDrop,
|
||||
isDeleted,
|
||||
clearImage,
|
||||
classes
|
||||
} = props;
|
||||
|
||||
const UploadImage = getComponent(options.uploadImageComponentName || 'UploadImage');
|
||||
|
||||
return (
|
||||
<FormControl component="fieldset" fullWidth={true} className={classes.root}>
|
||||
|
||||
<FormLabel component="legend" className={classes.label}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
{
|
||||
help &&
|
||||
|
||||
<FormHelperText>{help}</FormHelperText>
|
||||
}
|
||||
<div className={classes.uploadField}>
|
||||
{
|
||||
(disabled && !enableMultiple)
|
||||
?
|
||||
null
|
||||
:
|
||||
<Dropzone
|
||||
style={options.dropzoneStyle}
|
||||
multiple={enableMultiple}
|
||||
onDrop={onDrop}
|
||||
accept="image/*"
|
||||
className={classes.dropzoneBase}
|
||||
activeClassName={classes.dropzoneActive}
|
||||
rejectClassName={classes.dropzoneReject}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage id={`upload.${disabled ? 'maxReached' : 'prompt'}`}
|
||||
values={{ maxCount }}/>
|
||||
</div>
|
||||
{uploading && (
|
||||
<div className="upload-uploading">
|
||||
<span>
|
||||
<FormattedMessage id={`upload.uploading`}/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
}
|
||||
|
||||
{!!images.length && (
|
||||
<div className={classes.uploadState}>
|
||||
<div className={classes.uploadImages}>
|
||||
{images.map(
|
||||
(image, index) =>
|
||||
!isDeleted(index) && (
|
||||
<UploadImage
|
||||
clearImage={clearImage}
|
||||
key={index}
|
||||
index={index}
|
||||
image={image}
|
||||
loading={image.loading}
|
||||
preview={image.preview}
|
||||
error={image.error}
|
||||
style={options.imageStyle}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
UploadInner.propTypes = {
|
||||
uploading: PropTypes.bool,
|
||||
images: PropTypes.array.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
maxCount: PropTypes.number.isRequired,
|
||||
label: PropTypes.string,
|
||||
help: PropTypes.string,
|
||||
options: PropTypes.object.isRequired,
|
||||
enableMultiple: PropTypes.bool,
|
||||
onDrop: PropTypes.func.isRequired,
|
||||
isDeleted: PropTypes.func.isRequired,
|
||||
clearImage: PropTypes.func.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
UploadInner.displayName = 'UploadInnerMui';
|
||||
|
||||
|
||||
registerComponent('UploadInner', UploadInner, [withStyles, styles]);
|
12
packages/vulcan-ui-material/en_US.js
Normal file
12
packages/vulcan-ui-material/en_US.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { addStrings } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
addStrings('en', {
|
||||
|
||||
"search.search": "Search",
|
||||
"search.clear": "Clear search",
|
||||
"load_more.load_more": "Load more",
|
||||
"load_more.loaded_count": "Loaded {count} of {totalCount}",
|
||||
"load_more.loaded_all": "{totalCount, plural, =0 {No items} one {One item} other {# items}}",
|
||||
|
||||
});
|
98
packages/vulcan-ui-material/example/Header.jsx
Normal file
98
packages/vulcan-ui-material/example/Header.jsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import MenuIcon from 'mdi-material-ui/Menu';
|
||||
import ChevronLeftIcon from 'mdi-material-ui/ChevronLeft';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import { getSetting, Components, registerComponent } from 'meteor/vulcan:core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const drawerWidth = 240;
|
||||
const topBarHeight = 100;
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
appBar: {
|
||||
position: 'absolute',
|
||||
transition: theme.transitions.create(['margin', 'width'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(['margin', 'width'], {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
toolbar: {
|
||||
height: `${topBarHeight}px`,
|
||||
minHeight: `${topBarHeight}px`,
|
||||
},
|
||||
headerMid: {
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'& h1': {
|
||||
margin: '0 24px 0 0',
|
||||
fontSize: '18px',
|
||||
lineHeight: 1,
|
||||
}
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: theme.spacing.unit * 3,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const Header = (props, context) => {
|
||||
const classes = props.classes;
|
||||
const isSideNavOpen = props.isSideNavOpen;
|
||||
const toggleSideNav = props.toggleSideNav;
|
||||
|
||||
const siteTitle = getSetting('title', 'My App');
|
||||
|
||||
return (
|
||||
<AppBar className={classNames(classes.appBar, isSideNavOpen && classes.appBarShift)}>
|
||||
<Toolbar className={classes.toolbar}>
|
||||
|
||||
<IconButton
|
||||
aria-label="open drawer"
|
||||
onClick={e => toggleSideNav()}
|
||||
className={classNames(classes.menuButton)}
|
||||
color="inherit"
|
||||
>
|
||||
{isSideNavOpen ? <ChevronLeftIcon/> : <MenuIcon/>}
|
||||
</IconButton>
|
||||
|
||||
<div className={classNames(classes.headerMid)}>
|
||||
|
||||
<Typography variant="h6" color="inherit" className="tagline">
|
||||
{siteTitle}
|
||||
</Typography>
|
||||
|
||||
</div>
|
||||
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Header.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
isSideNavOpen: PropTypes.bool,
|
||||
toggleSideNav: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
|
||||
registerComponent('Header', Header, [withStyles, styles]);
|
136
packages/vulcan-ui-material/example/Layout.jsx
Normal file
136
packages/vulcan-ui-material/example/Layout.jsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import { Components, replaceComponent, Utils } from 'meteor/vulcan:core';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
const drawerWidth = 240;
|
||||
const topBarHeight = 100;
|
||||
|
||||
|
||||
const styles = theme => {
|
||||
const contentPadding = theme.spacing.unit * 8;
|
||||
|
||||
return {
|
||||
'@global': {
|
||||
html: {
|
||||
background: theme.palette.background.default,
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
MozOsxFontSmoothing: 'grayscale',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
body: {
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
root: {
|
||||
width: '100%',
|
||||
zIndex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
appFrame: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
drawerPaper: {
|
||||
position: 'relative',
|
||||
width: drawerWidth,
|
||||
backgroundColor: theme.palette.background[200],
|
||||
},
|
||||
drawerHeader: {
|
||||
height: `${topBarHeight}px !important`,
|
||||
minHeight: `${topBarHeight}px !important`,
|
||||
position: 'relative !important',
|
||||
},
|
||||
content: {
|
||||
padding: contentPadding,
|
||||
width: '100%',
|
||||
marginLeft: -drawerWidth,
|
||||
flexGrow: 1,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
height: `calc(100% - ${topBarHeight}px - ${contentPadding * 2}px)`,
|
||||
marginTop: topBarHeight,
|
||||
overflowY: 'scroll',
|
||||
},
|
||||
mainShift: {
|
||||
marginLeft: 0,
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
class Layout extends React.Component {
|
||||
state = {
|
||||
isOpen: { sideNav: true }
|
||||
};
|
||||
|
||||
toggle = (item, openOrClose) => {
|
||||
const newState = { isOpen: {} };
|
||||
newState.isOpen[item] = typeof openOrClose === 'string' ?
|
||||
openOrClose === 'open' :
|
||||
!this.state.isOpen[item];
|
||||
this.setState(newState);
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const routeName = Utils.slugify(this.props.currentRoute.name);
|
||||
const classes = this.props.classes;
|
||||
const isOpen = this.state.isOpen;
|
||||
|
||||
return (
|
||||
<div className={classNames(classes.root, 'wrapper', `wrapper-${routeName}`)}>
|
||||
<div className={classes.appFrame}>
|
||||
|
||||
<Components.Header isSideNavOpen={isOpen.sideNav}
|
||||
toggleSideNav={openOrClose =>
|
||||
this.toggle('sideNav', openOrClose)} />
|
||||
|
||||
<Drawer variant="persistent"
|
||||
classes={{ paper: classes.drawerPaper, }}
|
||||
open={isOpen.sideNav}
|
||||
>
|
||||
<AppBar className={classes.drawerHeader} elevation={4} square={true}>
|
||||
<Toolbar>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Components.SideNavigation />
|
||||
</Drawer>
|
||||
|
||||
<main className={classNames(classes.content, isOpen.sideNav && classes.mainShift)}>
|
||||
{this.props.children}
|
||||
</main>
|
||||
|
||||
<Components.FlashMessages />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Layout.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
|
||||
Layout.displayName = 'Layout';
|
||||
|
||||
|
||||
replaceComponent('Layout', Layout, [withStyles, styles]);
|
105
packages/vulcan-ui-material/example/SideNavigation.jsx
Normal file
105
packages/vulcan-ui-material/example/SideNavigation.jsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Components, registerComponent, withCurrentUser } from 'meteor/vulcan:core';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import ExpandLessIcon from 'mdi-material-ui/ChevronUp';
|
||||
import ExpandMoreIcon from 'mdi-material-ui/ChevronDown';
|
||||
import LockIcon from 'mdi-material-ui/Lock';
|
||||
import UsersIcon from 'mdi-material-ui/AccountMultiple';
|
||||
import ThemeIcon from 'mdi-material-ui/Palette';
|
||||
import HomeIcon from 'mdi-material-ui/Home';
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Users from 'meteor/vulcan:users';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {},
|
||||
nested: {
|
||||
paddingLeft: theme.spacing.unit * 4,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
class SideNavigation extends React.Component {
|
||||
state = {
|
||||
isOpen: { admin: false }
|
||||
};
|
||||
|
||||
toggle = (item) => {
|
||||
const newState = { isOpen: {} };
|
||||
newState.isOpen[item] = !this.state.isOpen[item];
|
||||
this.setState(newState);
|
||||
};
|
||||
|
||||
render () {
|
||||
const currentUser = this.props.currentUser;
|
||||
const classes = this.props.classes;
|
||||
const isOpen = this.state.isOpen;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
<List>
|
||||
<ListItem button onClick={() => {this.props.history.push('/');}}>
|
||||
<ListItemIcon>
|
||||
<HomeIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText inset primary="Home"/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{
|
||||
Users.isAdmin(currentUser) &&
|
||||
|
||||
<div>
|
||||
<Divider/>
|
||||
<List>
|
||||
<ListItem button onClick={e => this.toggle('admin')}>
|
||||
<ListItemIcon>
|
||||
<LockIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Admin"/>
|
||||
{isOpen.admin ? <ExpandLessIcon/> : <ExpandMoreIcon/>}
|
||||
</ListItem>
|
||||
<Collapse in={isOpen.admin} transitionduration="auto" unmountOnExit>
|
||||
<ListItem button className={classes.nested}
|
||||
onClick={() => {this.props.history.push('/admin');}}>
|
||||
<ListItemIcon>
|
||||
<UsersIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText inset primary="Users"/>
|
||||
</ListItem>
|
||||
<ListItem button className={classes.nested}
|
||||
onClick={() => {this.props.history.push('/theme');}}>
|
||||
<ListItemIcon>
|
||||
<ThemeIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText inset primary="Theme"/>
|
||||
</ListItem>
|
||||
</Collapse>
|
||||
</List>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SideNavigation.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
SideNavigation.displayName = 'SideNavigation';
|
||||
|
||||
|
||||
registerComponent('SideNavigation', SideNavigation, [withStyles, styles], withCurrentUser, withRouter);
|
16
packages/vulcan-ui-material/forms.css
Normal file
16
packages/vulcan-ui-material/forms.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.form-nested-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form-nested-item-inner {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.form-nested-item-remove {
|
||||
padding-top: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.form-nested-item-remove > button {
|
||||
margin-right: -4px;
|
||||
}
|
8
packages/vulcan-ui-material/fr_FR.js
Normal file
8
packages/vulcan-ui-material/fr_FR.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { addStrings } from 'meteor/vulcan:core';
|
||||
|
||||
addStrings('fr', {
|
||||
|
||||
"search.search": "Recherche",
|
||||
"search.clear": "Effacer la recherche",
|
||||
|
||||
});
|
109
packages/vulcan-ui-material/history.md
Normal file
109
packages/vulcan-ui-material/history.md
Normal file
|
@ -0,0 +1,109 @@
|
|||
1.12.8_17 / 2019-02-02
|
||||
======================
|
||||
|
||||
* TooltipIntl: Changed display from 'inline-block' to 'inherit' for more flexibility
|
||||
* Countries: Added getRegionLabel function
|
||||
|
||||
1.12.8_16 / 2019-01-21
|
||||
======================
|
||||
|
||||
* Countries: Fixed bug in validateRegion
|
||||
|
||||
1.12.8_15 / 2019-01-21
|
||||
======================
|
||||
|
||||
* Countries: Fixed bug in validateRegion
|
||||
|
||||
1.12.8_14 / 2019-01-20
|
||||
======================
|
||||
|
||||
* Countries: Added validateRegion function, which given a region value or label, will return the region value ('NY' or 'New York' => 'NY)
|
||||
* The contents of countries is now exported - this may be refactored out of the core vulcan-material-ui as some point
|
||||
|
||||
1.12.8_13 / 2019-01-14
|
||||
======================
|
||||
|
||||
* ModalTrigger: Added boolean dialogOverflow prop for use cases like popups that can go beyond the size of the dialog box
|
||||
* MuiSuggest: Fixed bug - The disabled state was not displayed correctly
|
||||
* MuiSuggest: Fixed bug - After selecting a suggestion, clicking on the control did not re-open the suggestions menu
|
||||
|
||||
1.12.8_12 / 2019-01-12
|
||||
======================
|
||||
|
||||
* Upgraded to Meteor 1.8.0.2
|
||||
|
||||
1.12.8_11 / 2018-12-21
|
||||
======================
|
||||
|
||||
* SearchInput: Added install autosize-input to readme
|
||||
* Datatable: Fixed sorting delay
|
||||
* Datatable: Added tableHeadCell class
|
||||
* Datatable: Added cellClass column property, which can be a string or a function: column.cellClass({ column, document, currentUser })
|
||||
|
||||
1.12.8_10 / 2018-12-09
|
||||
======================
|
||||
|
||||
* TooltipIntl: Added icon class
|
||||
* FormGroupWithLine: Moved caret from the right side to next to the title
|
||||
* Changed load_more.loaded_all string
|
||||
|
||||
1.12.8_9 / 2018-11-26
|
||||
=====================
|
||||
|
||||
* Fixed bug that displayed invalid total count at the bottom of data tables
|
||||
|
||||
1.12.8_8 / 2018-11-23
|
||||
=====================
|
||||
|
||||
* Improved the functionality of the LoadMore component
|
||||
* The showNoMore property has been deprecated
|
||||
* A showCount property has been added (true by default) that shows a count of loaded and total items
|
||||
* The load more icon or button is displayed even when infiniteScroll is enabled
|
||||
|
||||
1.12.8_7 / 2018-11-10
|
||||
=====================
|
||||
|
||||
* Fixed bug in Datatable.jsx
|
||||
* Updated ReadMe
|
||||
|
||||
1.12.8_6 / 2018-11-06
|
||||
=====================
|
||||
|
||||
* Fixed bug in Datatable.jsx
|
||||
* Reduced spacing of form components
|
||||
|
||||
1.12.8_5 / 2018-10-31
|
||||
=====================
|
||||
|
||||
* Fixed bugs in Datatable pagination
|
||||
* Set Datatable paginate prop to false by default
|
||||
|
||||
1.12.8_4 / 2018-10-31
|
||||
=====================
|
||||
|
||||
* Removed 'fr_FR.js' from package.js because any french strings loaded activates the french language
|
||||
* Fixed delete button and its tooltips positioning in FormSubmit
|
||||
* Added pagination to Datatable
|
||||
|
||||
1.12.8_2 / 2018-10-29
|
||||
=====================
|
||||
|
||||
* Fixed localization in "clear search" tooltip
|
||||
* Added name and aria-haspopup properties to the input component to improve compliance and facilitate UAT
|
||||
* Replaced Date, Time and DateTime form controls with native controls as recommended by MUI.
|
||||
The deprecated react-datetime version of the controls are still there as DateRdt, TimeRdt and DateTimeRdt, but they are not registered.
|
||||
* Updated readme
|
||||
|
||||
1.12.8_1 / 2018-10-22
|
||||
=====================
|
||||
|
||||
* Made form components compatible with new Form.formComponents property
|
||||
|
||||
1.12.8 / 2018-10-19
|
||||
===================
|
||||
|
||||
* Made improvements to the search box, including keyboard shortcuts (s: focus search; c: clear search)
|
||||
* Added support in TooltipIntl for tooltips in popovers
|
||||
* Added action prop to ModalTrigger that enables a parent component to call openModal and closeModal
|
||||
* Started using MUI tables in Card component
|
||||
* Fixed bugs in MuiSuggest component
|
4
packages/vulcan-ui-material/modules/index.js
Normal file
4
packages/vulcan-ui-material/modules/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './themes';
|
||||
export JssCleanup from '../components/theme/JssCleanup';
|
||||
import './sampleTheme';
|
||||
import './routes';
|
8
packages/vulcan-ui-material/modules/routes.js
Executable file
8
packages/vulcan-ui-material/modules/routes.js
Executable file
|
@ -0,0 +1,8 @@
|
|||
import { addRoute } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
addRoute({
|
||||
name: 'theme',
|
||||
path: '/theme',
|
||||
componentName: 'ThemeStyles',
|
||||
});
|
76
packages/vulcan-ui-material/modules/sampleTheme.js
Normal file
76
packages/vulcan-ui-material/modules/sampleTheme.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { registerTheme } from './themes';
|
||||
import indigo from '@material-ui/core/colors/indigo';
|
||||
import blue from '@material-ui/core/colors/blue';
|
||||
import red from '@material-ui/core/colors/red';
|
||||
|
||||
|
||||
/** @ignore */
|
||||
|
||||
/**
|
||||
*
|
||||
* Sample theme to get you out of the gate quickly
|
||||
*
|
||||
* For a complete list of configuration variables see:
|
||||
* https://material-ui.com/customization/themes/
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
const theme = {
|
||||
|
||||
palette: {
|
||||
primary: indigo,
|
||||
secondary: blue,
|
||||
error: red,
|
||||
},
|
||||
|
||||
utils: {
|
||||
|
||||
tooltipEnterDelay: 700,
|
||||
|
||||
errorMessage: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: red[500],
|
||||
color: 'white',
|
||||
borderRadius: '4px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
denseTable: {
|
||||
'& > thead > tr > th, & > tbody > tr > td': {
|
||||
padding: '4px 16px 4px 16px',
|
||||
},
|
||||
'& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
|
||||
paddingRight: '16px',
|
||||
},
|
||||
},
|
||||
|
||||
flatTable: {
|
||||
'& > thead > tr > th, & > tbody > tr > td': {
|
||||
padding: '4px 16px 4px 16px',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
'& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
|
||||
paddingRight: '16px',
|
||||
},
|
||||
},
|
||||
|
||||
denserTable: {
|
||||
'& > thead > tr, & > tbody > tr': {
|
||||
height: '40px',
|
||||
},
|
||||
'& > thead > tr > th, & > tbody > tr > td': {
|
||||
padding: '4px 16px 4px 16px',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
'& > thead > tr > th:last-child, & > tbody > tr > td:last-child': {
|
||||
paddingRight: '16px',
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
||||
registerTheme('Sample', theme);
|
67
packages/vulcan-ui-material/modules/themes.js
Normal file
67
packages/vulcan-ui-material/modules/themes.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/** @module vulcan-material-ui */
|
||||
|
||||
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
|
||||
import { registerSetting, getSetting } from 'meteor/vulcan:core';
|
||||
|
||||
|
||||
registerSetting('muiTheme', 'Sample', 'Material UI theme used by erikdakota:vulcan-material-ui');
|
||||
|
||||
|
||||
export const ThemesTable = {}; // storage for info about themes
|
||||
|
||||
|
||||
/**
|
||||
* Register a theme with a name
|
||||
*
|
||||
* @param {String} name The name of the theme to register
|
||||
* @param {Object} theme The theme object - see defaultTheme.js
|
||||
*
|
||||
*/
|
||||
export const registerTheme = (name, theme) => {
|
||||
const themeInfo = {
|
||||
name,
|
||||
theme,
|
||||
};
|
||||
|
||||
ThemesTable[name] = themeInfo;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a theme registered with registerTheme()
|
||||
*
|
||||
* @param {String} name The name of the theme to get
|
||||
*
|
||||
* @returns {Object} A theme object
|
||||
*/
|
||||
export const getTheme = (name) => {
|
||||
const themeInfo = ThemesTable[name];
|
||||
if (!themeInfo) return null;
|
||||
themeInfo.theme.typography = { ...themeInfo.theme.typography, useNextVariants: true }
|
||||
return createMuiTheme(themeInfo.theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the raw theme object registered with registerTheme()
|
||||
*
|
||||
* @param {String} name The name of the theme to get
|
||||
*
|
||||
* @returns {Object} The object passed to registerTheme
|
||||
*/
|
||||
|
||||
export const getRawTheme = (name) => {
|
||||
const themeInfo = ThemesTable[name];
|
||||
if (!themeInfo) return null;
|
||||
return themeInfo.theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the theme specified in the 'muiTheme' setting
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const getCurrentTheme = () => {
|
||||
const themeName = getSetting('muiTheme', 'Sample');
|
||||
const theme = getTheme(themeName);
|
||||
return theme;
|
||||
};
|
26
packages/vulcan-ui-material/package.js
Normal file
26
packages/vulcan-ui-material/package.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
Package.describe({
|
||||
name: 'vulcan:ui-material',
|
||||
version: '1.12.17',
|
||||
summary: 'Replacement for Vulcan (http://vulcanjs.org/) components using material-ui',
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.versionsFrom('METEOR@1.6');
|
||||
|
||||
api.use([
|
||||
'ecmascript',
|
||||
'vulcan:core@1.12.8',
|
||||
'vulcan:accounts@1.12.8',
|
||||
'vulcan:forms@1.12.8',
|
||||
]);
|
||||
|
||||
api.addFiles([
|
||||
'accounts.css',
|
||||
'forms.css',
|
||||
], ['client', 'server']);
|
||||
|
||||
api.mainModule('client/main.js', 'client');
|
||||
api.mainModule('server/main.js', 'server');
|
||||
});
|
208
packages/vulcan-ui-material/readme.md
Normal file
208
packages/vulcan-ui-material/readme.md
Normal file
|
@ -0,0 +1,208 @@
|
|||
# erikdakoda:vulcan-material-ui 1.12.8_13
|
||||
|
||||
Replacement for [Vulcan](http://vulcanjs.org/) components using [Material-UI](https://material-ui.com/).
|
||||
This version has been tested against Vulcan 1.12.8 and Material-UI 3.1.0.
|
||||
|
||||
To give me feedback open an issue on GitHub or you can reach me on the [Vulcan Slack](https://vulcanjs.slack.com)
|
||||
channel as erikdakoda.
|
||||
|
||||
There are some nice bonus features like a CountrySelect with autocomplete and theming.
|
||||
|
||||
All components in vulcan:ui-bootstrap, vulcan:forms and vulcan:accounts have been implemented except for Icon.
|
||||
|
||||
## Installation
|
||||
|
||||
To add vulcan-material-ui to an existing Vulcan project, enter the following:
|
||||
|
||||
``` sh
|
||||
meteor add erikdakoda:vulcan-material-ui
|
||||
|
||||
meteor npm install --save @material-ui/core
|
||||
meteor npm install --save react-jss
|
||||
meteor npm install --save mdi-material-ui
|
||||
meteor npm install --save react-autosuggest
|
||||
meteor npm install --save autosuggest-highlight
|
||||
meteor npm install --save react-isolated-scroll
|
||||
meteor npm install --save-exact react-keyboard-event-handler@1.3.2
|
||||
meteor npm install --save autosize-input
|
||||
```
|
||||
|
||||
> NOTE: If you want to avoid deprecation warnings added in MUI versions after 3.1.0, you can lock MUI to the currently supported version using `meteor npm install --save @material-ui/core@3.1.0`. Don't for get to remove or update the version number when you update this package in the future.
|
||||
|
||||
|
||||
> IMPORTANT: Please note that I have abandoned material-ui-icons in favor of mdi-material-ui because it has a much larger [selection of icons](https://materialdesignicons.com/).
|
||||
|
||||
This package no longer depends on `vulcan:ui-boostrap`, so you can remove it.
|
||||
|
||||
To activate the example layout copy the three components to your project and import them:
|
||||
|
||||
``` javascript
|
||||
import './example/Header',
|
||||
import './example/Layout',
|
||||
import './example/SideNavigation',
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
For an example theme see `modules/sampleTheme.js`. For a complete list of values you can customize,
|
||||
see the [MUI Default Theme](https://material-ui-next.com/customization/default-theme/).
|
||||
|
||||
Register your theme in the Vulcan environment by giving it a name: `registerTheme('MyTheme', theme);`.
|
||||
You can have multiple themes registered and you can specify which one to use in your settings file using the `muiTheme` public setting.
|
||||
|
||||
In addition to the Material UI spec, I use a `utils` section in my themes where I place global variables for reusable styles.
|
||||
For example the sample theme contains
|
||||
|
||||
```
|
||||
const theme = {
|
||||
|
||||
. . .
|
||||
|
||||
utils: {
|
||||
|
||||
tooltipEnterDelay: 700,
|
||||
|
||||
errorMessage: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: red[500],
|
||||
color: 'white',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
|
||||
. . .
|
||||
|
||||
// additional utils definitions go here
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
```
|
||||
|
||||
You can use tooltipEnterDelay (or any other variable you define in utils) anywhere you include the withTheme HOC. See `/components/bonus/TooltipIconButton.jsx` for an example.
|
||||
|
||||
You can use errorMessage (or any other style fragment you define in utils) anywhere you include the withStyles HOC. See `/components/accounts/Form.jsx` for an example.
|
||||
|
||||
## Server Side Rendering (SSR)
|
||||
|
||||
Material UI and Vulcan support SSR, but this is a complex beast with pitfalls. Sometimes you will see a warning like this:
|
||||
|
||||
`Warning: Prop className did not match. Server: "MuiChip-label-131" Client: "MuiChip-label-130"`
|
||||
|
||||
Sometimes the React rendered on the server and the client don't match exactly and this causes a problem with [JSS](https://material-ui-next.com/customization/css-in-js/#jss). This is a complicated issue that has multiple causes and I will be working on solving each of the issues causing this over time.
|
||||
|
||||
Your pages should still render correctly, but there may be a blink and redraw when the first page after SSR loads in the browser.
|
||||
|
||||
In your own code, make sure that your components will render the same on the server and the client. This means not referring to client-side object such as `document` or `jQuery`. If you have a misbehaving component, try wrapping it with [react-no-ssr](https://github.com/kadirahq/react-no-ssr).
|
||||
|
||||
## Form Controls
|
||||
|
||||
You can pass a couple of extra options to inputs from the `form` property of your schema:
|
||||
|
||||
``` javascript
|
||||
userKey: {
|
||||
type: String,
|
||||
label: 'User key',
|
||||
description: 'The user’s key',
|
||||
optional: true,
|
||||
hidden: function ({ document }) {
|
||||
return !document.platformId || !document.usePlatformApp;
|
||||
},
|
||||
inputProperties: {
|
||||
autoFocus: true, // focus this input when the form loads
|
||||
addonBefore: <KeyIcon/>, // adorn the start of the input
|
||||
addonAfter: <KeyIcon/>, // adorn the end of the input
|
||||
inputClassName: 'halfWidthLeft', // use 'halfWidthLeft' or 'halfWidthRight'
|
||||
// to display two controls side by side
|
||||
hideLabel: true, // hide the label
|
||||
rows: 10, // for textareas you can specify the rows
|
||||
variant: 'switch', // for checkboxgroups you can use either
|
||||
// 'checkbox' (default) or 'switch'
|
||||
inputProps: { step: 'any' } // Attributes applied to the input element, for
|
||||
// ex pass the step attr to a number input
|
||||
},
|
||||
group: platformGroup,
|
||||
canRead: ['members'],
|
||||
canCreate: ['members'],
|
||||
canUpdate: ['members'],
|
||||
},
|
||||
```
|
||||
|
||||
> Note: `form.getHidden` has been deprecated. Now you can just pass a function to `hidden`.
|
||||
|
||||
## Form Groups
|
||||
|
||||
You can pass a couple of extra options to form groups as well:
|
||||
|
||||
``` javascript
|
||||
const platformGroup: {
|
||||
name: 'shops.platform',
|
||||
order: 4,
|
||||
startComponent: 'ShopsPlatformTitle', // component to put at the top of the form group
|
||||
endComponent: 'ShopsConnectButtons', // component to put at the bottom of the form group
|
||||
},
|
||||
```
|
||||
|
||||
## DataTable
|
||||
|
||||
You can pass the DataTable component an `editComponent` property in addition to or instead of `showEdit`. Here is a simplified example:
|
||||
|
||||
``` javascript
|
||||
const AgendaJobActions = ({ collection, document }) => {
|
||||
const scheduleAgent = () => {
|
||||
Meteor.call('scheduleAgent', document.agentId);
|
||||
};
|
||||
|
||||
return <Components.TooltipIconButton titleId="executions.execute_now"
|
||||
icon={<Components.ExecutionsIcon/>}
|
||||
onClick={scheduleAgent}/>;
|
||||
};
|
||||
|
||||
AgendaJobActionsInner.propTypes = {
|
||||
collecion: PropTypes.object.isRequired,
|
||||
document: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
<Components.Datatable
|
||||
editComponent={AgendaJobActions}
|
||||
collection={AgendaJobs}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
You can also control the spacing of the table cells using the `dense` property. Valid values are:
|
||||
|
||||
| Value | Description |
|
||||
| ------- | ------------ |
|
||||
| dense | right cell padding of 16px instead of 56px |
|
||||
| flat | right cell padding of 16px and nowrap |
|
||||
| denser | right cell padding of 16px, nowrap, and row height of 40px instead of 56px |
|
||||
|
||||
You can also use other string values, as long as you define a `utils` entry named the same + `Table`, for example `myCustomTable`. Check out the sample theme for examples.
|
||||
|
||||
|
||||
## CountrySelect
|
||||
|
||||
There is an additional component, an autosuggest-based country select.
|
||||
|
||||
``` javascript
|
||||
country: {
|
||||
type: String,
|
||||
label: 'Country',
|
||||
input: 'CountrySelect',
|
||||
canRead: ['guests'],
|
||||
canCreate: ['members'],
|
||||
canUpdate: ['members'],
|
||||
},
|
||||
```
|
||||
|
||||
Countries are stored as their 2-letter country codes. I have included a helper function for displaying the country name:
|
||||
|
||||
``` javascript
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { getCountryLabel } from 'meteor/erikdakoda:vulcan-material-ui';
|
||||
|
||||
<Typography variant="subtitle1">
|
||||
{getCountryLabel(supplier.country)}
|
||||
</Typography>
|
||||
```
|
||||
|
3
packages/vulcan-ui-material/server/main.js
Normal file
3
packages/vulcan-ui-material/server/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from '../components/index';
|
||||
export * from '../modules/index';
|
||||
import './wrapWithMuiTheme';
|
43
packages/vulcan-ui-material/server/wrapWithMuiTheme.jsx
Normal file
43
packages/vulcan-ui-material/server/wrapWithMuiTheme.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { addCallback } from 'meteor/vulcan:core';
|
||||
import JssProvider from 'react-jss/lib/JssProvider';
|
||||
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
|
||||
import createGenerateClassName from '@material-ui/core/styles/createGenerateClassName';
|
||||
import { getCurrentTheme } from '../modules/themes';
|
||||
import { SheetsRegistry } from 'react-jss/lib/jss';
|
||||
import JssCleanup from '../components/theme/JssCleanup';
|
||||
|
||||
|
||||
function wrapWithMuiTheme (app, {context }) {
|
||||
const sheetsRegistry = new SheetsRegistry();
|
||||
context.sheetsRegistry = sheetsRegistry;
|
||||
|
||||
const sheetsManager = new Map();
|
||||
|
||||
const theme = getCurrentTheme();
|
||||
|
||||
const generateClassName = createGenerateClassName({ seed: '' });
|
||||
|
||||
return (
|
||||
<JssProvider registry={sheetsRegistry} generateClassName={generateClassName}>
|
||||
<MuiThemeProvider theme={theme} sheetsManager={sheetsManager}>
|
||||
<JssCleanup>
|
||||
{app}
|
||||
</JssCleanup>
|
||||
</MuiThemeProvider>
|
||||
</JssProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function injectJss(sink, { context }) {
|
||||
const sheets = context.sheetsRegistry.toString();
|
||||
sink.appendToHead(
|
||||
`<style id="jss-server-side">${sheets}</style>`
|
||||
);
|
||||
return sink;
|
||||
}
|
||||
|
||||
|
||||
addCallback('router.server.wrapper', wrapWithMuiTheme);
|
||||
addCallback('router.server.postRender', injectJss);
|
Loading…
Add table
Reference in a new issue