diff --git a/.eslintrc b/.eslintrc index e175ad797..845d06457 100644 --- a/.eslintrc +++ b/.eslintrc @@ -55,7 +55,7 @@ "avoid-escape" ], "react/prop-types": 0, - "semi": [1, "always"] + "semi": [2, "always"] }, "env": { "browser": true, diff --git a/package-lock.json b/package-lock.json index adbaeba4d..279f2043d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/vulcan-ui-material/.editorconfig b/packages/vulcan-ui-material/.editorconfig new file mode 100644 index 000000000..68b3a5241 --- /dev/null +++ b/packages/vulcan-ui-material/.editorconfig @@ -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 diff --git a/packages/vulcan-ui-material/.eslintrc b/packages/vulcan-ui-material/.eslintrc new file mode 100755 index 000000000..39277e6db --- /dev/null +++ b/packages/vulcan-ui-material/.eslintrc @@ -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 + } +} diff --git a/packages/vulcan-ui-material/.gitignore b/packages/vulcan-ui-material/.gitignore new file mode 100644 index 000000000..e0ac94aec --- /dev/null +++ b/packages/vulcan-ui-material/.gitignore @@ -0,0 +1,3 @@ +npm-debug.log +node_modules +.idea/workspace.xml diff --git a/packages/vulcan-ui-material/.versions b/packages/vulcan-ui-material/.versions new file mode 100644 index 000000000..5e93359dd --- /dev/null +++ b/packages/vulcan-ui-material/.versions @@ -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 diff --git a/packages/vulcan-ui-material/accounts.css b/packages/vulcan-ui-material/accounts.css new file mode 100644 index 000000000..73485abcf --- /dev/null +++ b/packages/vulcan-ui-material/accounts.css @@ -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; +} + diff --git a/packages/vulcan-ui-material/client/main.js b/packages/vulcan-ui-material/client/main.js new file mode 100644 index 000000000..1ed6f2fb0 --- /dev/null +++ b/packages/vulcan-ui-material/client/main.js @@ -0,0 +1,3 @@ +export * from '../components/index'; +export * from '../modules/index'; +import './wrapWithMuiTheme'; diff --git a/packages/vulcan-ui-material/client/wrapWithMuiTheme.jsx b/packages/vulcan-ui-material/client/wrapWithMuiTheme.jsx new file mode 100644 index 000000000..8b0c3baa7 --- /dev/null +++ b/packages/vulcan-ui-material/client/wrapWithMuiTheme.jsx @@ -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 ( + + {this.props.children} + + ); + } +} + +registerComponent('ThemeProvider', ThemeProvider); + +function wrapWithMuiTheme(app) { + return ( + + {app} + + ); +} + + +addCallback('router.client.wrapper', wrapWithMuiTheme); diff --git a/packages/vulcan-ui-material/components/accounts/AccountsButton.jsx b/packages/vulcan-ui-material/components/accounts/AccountsButton.jsx new file mode 100755 index 000000000..d80dfe629 --- /dev/null +++ b/packages/vulcan-ui-material/components/accounts/AccountsButton.jsx @@ -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 ( + + ); + } +} + + +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); diff --git a/packages/vulcan-ui-material/components/accounts/AccountsButtons.jsx b/packages/vulcan-ui-material/components/accounts/AccountsButtons.jsx new file mode 100755 index 000000000..dc55093d9 --- /dev/null +++ b/packages/vulcan-ui-material/components/accounts/AccountsButtons.jsx @@ -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 ( + + {Object.keys(buttons).map((id, i) => + + )} + + ); + } +} + + +AccountsButtons.propTypes = { + classes: PropTypes.object.isRequired, + buttons: PropTypes.object, + className: PropTypes.string, +}; + + +AccountsButtons.displayName = 'AccountsButtons'; + + +replaceComponent('AccountsButtons', AccountsButtons, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/accounts/AccountsField.jsx b/packages/vulcan-ui-material/components/accounts/AccountsField.jsx new file mode 100755 index 000000000..4ac940a51 --- /dev/null +++ b/packages/vulcan-ui-material/components/accounts/AccountsField.jsx @@ -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
{label}
; + } + + 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 && + +
+ { this.input = ref; }} + onChange={onChange} + placeholder={hint} + defaultValue={defaultValue} + autoComplete={autoComplete } + label={label} + autoFocus={autoFocus} + required={required} + error={!!message} + helperText={message && message.message} + fullWidth + /> +
+ ); + } +} + + +AccountsField.propTypes = { + onChange: PropTypes.func, +}; + + +replaceComponent('AccountsField', AccountsField); diff --git a/packages/vulcan-ui-material/components/accounts/AccountsFields.jsx b/packages/vulcan-ui-material/components/accounts/AccountsFields.jsx new file mode 100755 index 000000000..59309c9a3 --- /dev/null +++ b/packages/vulcan-ui-material/components/accounts/AccountsFields.jsx @@ -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 ( + + { + Object.keys(fields).map((id, i) => + + ) + } + + ); + } +} + + +replaceComponent('AccountsFields', AccountsFields); diff --git a/packages/vulcan-ui-material/components/accounts/AccountsForm.jsx b/packages/vulcan-ui-material/components/accounts/AccountsForm.jsx new file mode 100755 index 000000000..af28fb37a --- /dev/null +++ b/packages/vulcan-ui-material/components/accounts/AccountsForm.jsx @@ -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 ( +
this.form = ref} + className={classNames(className, 'accounts-ui', { 'ready': ready, })} + noValidate + > + + + + + + + ); + } + + +} + + +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]); diff --git a/packages/vulcan-ui-material/components/accounts/AccountsPasswordOrService.jsx b/packages/vulcan-ui-material/components/accounts/AccountsPasswordOrService.jsx new file mode 100644 index 000000000..8f3a40667 --- /dev/null +++ b/packages/vulcan-ui-material/components/accounts/AccountsPasswordOrService.jsx @@ -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 ( + + { `${this.context.intl.formatMessage({id: 'accounts.or_use'})} ${ labels.join(' / ') }` } + + ); + } + return null; + } +} + +AccountsPasswordOrService.propTypes = { + oauthServices: PropTypes.object +}; + +AccountsPasswordOrService.contextTypes = { + intl: intlShape +}; + +replaceComponent('AccountsPasswordOrService', AccountsPasswordOrService, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/accounts/AccountsSocialButtons.jsx b/packages/vulcan-ui-material/components/accounts/AccountsSocialButtons.jsx new file mode 100644 index 000000000..2ce19f254 --- /dev/null +++ b/packages/vulcan-ui-material/components/accounts/AccountsSocialButtons.jsx @@ -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( + + {Object.keys(oauthServices).map((id, i) => { + return ; + })} + + ); + } +} + +replaceComponent('AccountsSocialButtons', AccountsSocialButtons, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/bonus/LoadMore.jsx b/packages/vulcan-ui-material/components/bonus/LoadMore.jsx new file mode 100644 index 000000000..934da0c7d --- /dev/null +++ b/packages/vulcan-ui-material/components/bonus/LoadMore.jsx @@ -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 + ? + + : + loadMore()}> + + ; + + return ( +
+ { + showCount && + + + + + } + { + isLoadingMore + + ? + + + + : + + hasMore + + ? + + infiniteScroll + + ? + + loadMore()}> + {loadMoreButton} + + + : + + loadMoreButton + : + + null + } +
+ ); +}; + + +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]); + diff --git a/packages/vulcan-ui-material/components/bonus/ScrollTrigger.jsx b/packages/vulcan-ui-material/components/bonus/ScrollTrigger.jsx new file mode 100644 index 000000000..0340f2536 --- /dev/null +++ b/packages/vulcan-ui-material/components/bonus/ScrollTrigger.jsx @@ -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 ( +
{this.element = element;}}> + {children} +
+ ); + } +} + + +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; diff --git a/packages/vulcan-ui-material/components/bonus/SearchInput.jsx b/packages/vulcan-ui-material/components/bonus/SearchInput.jsx new file mode 100644 index 000000000..fa611fdd0 --- /dev/null +++ b/packages/vulcan-ui-material/components/bonus/SearchInput.jsx @@ -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 = ; + + const clearButton = } + 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 ( + + this.input = input} + value={this.state.value} + type="search" + onChange={this.updateSearch} + onFocus={this.handleFocus} + disableUnderline={true} + startAdornment={searchIcon} + endAdornment={clearButton} + /> + + { + // KeyboardEventHandler is not valid on the server, where its name is undefined + typeof window !== 'undefined' && KeyboardEventHandler.name && !noShortcuts && + + + } + + + ); + } + +} + + +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]); diff --git a/packages/vulcan-ui-material/components/bonus/TooltipIconButton.jsx b/packages/vulcan-ui-material/components/bonus/TooltipIconButton.jsx new file mode 100644 index 000000000..45e3ba288 --- /dev/null +++ b/packages/vulcan-ui-material/components/bonus/TooltipIconButton.jsx @@ -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 ( + +
+ { + variant === 'fab' + + ? + + + + : + + + {icon} + + } +
+
+ ); + +}; + + +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]); diff --git a/packages/vulcan-ui-material/components/bonus/TooltipIntl.jsx b/packages/vulcan-ui-material/components/bonus/TooltipIntl.jsx new file mode 100644 index 000000000..5e21e277d --- /dev/null +++ b/packages/vulcan-ui-material/components/bonus/TooltipIntl.jsx @@ -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 ( + + + + { + variant === 'fab' && !!icon + + ? + + + + : + + !!icon + + ? + + + {iconWithClass} + + + : + + variant === 'button' + + ? + + + : + + children + } + + + + ); + +}; + + +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]); diff --git a/packages/vulcan-ui-material/components/core/Card.jsx b/packages/vulcan-ui-material/components/core/Card.jsx new file mode 100644 index 000000000..a311c4841 --- /dev/null +++ b/packages/vulcan-ui-material/components/core/Card.jsx @@ -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 ? + {value}/ : + ; +}; + + +const LimitedString = ({ string }) => +
+ {string.indexOf(' ') === -1 && string.length > 30 ? + {string.substr(0, 30)}… : + {string} + } +
; + + +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 ; + + case 'Number': + case 'number': + case 'SimpleSchema.Integer': + return {value.toString()}; + + case 'Array': + return
    {value.map( + (item, index) =>
  1. {getFieldValue(item, typeof item, classes)}
  2. )}
; + + case 'Object': + case 'object': + return ( + + + {_.map(value, (value, key) => + + {key} + {getFieldValue(value, typeof value, classes)} + + )} + +
+ ); + + 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 }) => + + + {label} + + + {getFieldValue(value, typeName, classes)} + + ; + + +const CardEdit = (props, context) => { + const classes = props.classes; + const editTitle = context.intl.formatMessage({ id: 'cards.edit' }); + return ( + + + + + } + > + + + + + ); +}; + + +CardEdit.contextTypes = { intl: intlShape }; + + +const CardEditForm = ({ collection, document, closeModal }) => + { + 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 ( +
+ + + {canUpdate ? : null} + {fieldNames.map((fieldName, index) => + + )} + +
+
+ ); +}; + + +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]); diff --git a/packages/vulcan-ui-material/components/core/Datatable.jsx b/packages/vulcan-ui-material/components/core/Datatable.jsx new file mode 100644 index 000000000..982a404d6 --- /dev/null +++ b/packages/vulcan-ui-material/components/core/Datatable.jsx @@ -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 ; + + } 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 ( +
+ {/* DatatableAbove Component part*/} + { + showSearch && + + + } + { + showNew && + + + } + + +
+ ); + } + } +} + + +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 ; + } 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 ( + + { + title && + + + title + + + } + + + + { + _.sortBy(columns, column => column.order).map( + (column, index) => + + ) + } + { + (showEdit || editComponent) && + + + } + + + + { + results && + + + { + results.map( + (document, index) => + ) + } + + } + + { + footerData && + + + + { + _.sortBy(columns, column => column.order).map( + (column, index) => + + {footerData[index]} + + ) + } + { + (showEdit || editComponent) && + + + } + + + + } + +
+ { + paginate && + + + } + { + !paginate && loadMore && + + + } +
+ ); +}; + + +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 ; + } + } else if (intlNamespace) { + formattedLabel = typeof columnName === 'string' ? + intl.formatMessage({ + id: `${intlNamespace}.${columnName}`, + defaultMessage: columnName + }) : + ''; + } else { + formattedLabel = intl.formatMessage({ id: columnName, defaultMessage: columnName }); + } + + return {formattedLabel}; +}; + + +DatatableHeader.contextTypes = { + intl: intlShape, +}; + + +replaceComponent('DatatableHeader', DatatableHeader); + + +/* + +DatatableSorter Component + +*/ + +const DatatableSorter = ({ name, label, toggleSort, currentSort, sortable }) => + + + toggleSort(name)} + > + {label} + + + ; + +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 ( + handleRowClick(event, document))} + hover + > + + { + _.sortBy(columns, column => column.order).map( + (column, index) => + ) + } + + { + (showEdit || editComponent) && + + + { + EditComponent && + + + } + { + showEdit && + + + } + + } + + + ); +}; + + +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 ( + + + + ); +}; + + +replaceComponent('DatatableCell', DatatableCell); + + +/* + +DatatableDefaultCell Component + +*/ +const DatatableDefaultCell = ({ column, document }) => +
+ { + typeof column === 'string' + ? + getFieldValue(document[column]) + : + getFieldValue(document[column.name]) + } +
; + + +replaceComponent('DatatableDefaultCell', DatatableDefaultCell); diff --git a/packages/vulcan-ui-material/components/core/EditButton.jsx b/packages/vulcan-ui-material/components/core/EditButton.jsx new file mode 100644 index 000000000..7e391a648 --- /dev/null +++ b/packages/vulcan-ui-material/components/core/EditButton.jsx @@ -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 }) => ( + + } + color={color} + variant={variant} + classes={buttonClasses} + />} + > + + +); + + +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 ( + +); +} + +registerComponent('EditForm', EditForm); diff --git a/packages/vulcan-ui-material/components/core/Loading.jsx b/packages/vulcan-ui-material/components/core/Loading.jsx new file mode 100644 index 000000000..dc821a032 --- /dev/null +++ b/packages/vulcan-ui-material/components/core/Loading.jsx @@ -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 ; +} + +replaceComponent('Loading', Loading); diff --git a/packages/vulcan-ui-material/components/core/ModalTrigger.jsx b/packages/vulcan-ui-material/components/core/ModalTrigger.jsx new file mode 100644 index 000000000..8a3dbd159 --- /dev/null +++ b/packages/vulcan-ui-material/components/core/ModalTrigger.jsx @@ -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' + ? + + : + {label}; + + const childrenComponent = typeof children.type === 'function' ? + React.cloneElement(children, { closeModal: this.closeModal }) : + children; + + return ( + + + {triggerComponent} + + + + { + title && + + {title} + } + + + {childrenComponent} + + + + + + ); + } +} + + +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]); diff --git a/packages/vulcan-ui-material/components/core/NewButton.jsx b/packages/vulcan-ui-material/components/core/NewButton.jsx new file mode 100644 index 000000000..1ce2cbf93 --- /dev/null +++ b/packages/vulcan-ui-material/components/core/NewButton.jsx @@ -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 }) => ( + + } + color={color} + variant={variant} + />} + > + + +); + + +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); diff --git a/packages/vulcan-ui-material/components/forms/FormComponentInner.jsx b/packages/vulcan-ui-material/components/forms/FormComponentInner.jsx new file mode 100644 index 000000000..473f39e6d --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormComponentInner.jsx @@ -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 ; + } else if (nestedInput){ + return ; + } else { + return ( +
+ {instantiateComponent(beforeComponent, properties)} + + {instantiateComponent(afterComponent, properties)} +
+ ); + } + + } +} + + +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]); diff --git a/packages/vulcan-ui-material/components/forms/FormErrors.jsx b/packages/vulcan-ui-material/components/forms/FormErrors.jsx new file mode 100644 index 000000000..049d13ce1 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormErrors.jsx @@ -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 = ( +
    + {errors.map((error, index) => ( +
  • + +
  • + ))} +
+ ); + + return ( +
+ {!!errors.length && ( + + )} +
+ ); +}; + + +replaceComponent('FormErrors', FormErrors, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/forms/FormGroup.jsx b/packages/vulcan-ui-material/components/forms/FormGroup.jsx new file mode 100644 index 000000000..3ad7781db --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormGroup.jsx @@ -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 ( + + +
+ {label} +
+ + { + collapsible && + +
+ { + this.state.collapsed + ? + + : + + } +
+ } + +
+ ); + }; + + + // 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 ( + + ); + } + + +} + + +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]); diff --git a/packages/vulcan-ui-material/components/forms/FormGroupNone.jsx b/packages/vulcan-ui-material/components/forms/FormGroupNone.jsx new file mode 100644 index 000000000..773b11eba --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormGroupNone.jsx @@ -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 ( + + ); + } + + +} + + +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]); diff --git a/packages/vulcan-ui-material/components/forms/FormGroupWithLine.jsx b/packages/vulcan-ui-material/components/forms/FormGroupWithLine.jsx new file mode 100644 index 000000000..7ea0c0965 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormGroupWithLine.jsx @@ -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 ( +
+ + + + +
+ {this.props.label} +
+ { + collapsible && + +
+ { + this.state.collapsed + ? + + : + + } +
+ } +
+ +
+ ); + }; + + + // 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 ( +
+ ); + } +} + + +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]); diff --git a/packages/vulcan-ui-material/components/forms/FormNested.jsx b/packages/vulcan-ui-material/components/forms/FormNested.jsx new file mode 100644 index 000000000..2b3c7e5db --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormNested.jsx @@ -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 = () => ; + +replaceComponent('IconRemove', IconRemove); + +const IconAdd = () => ; + +replaceComponent('IconAdd', IconAdd); diff --git a/packages/vulcan-ui-material/components/forms/FormNestedDivider.jsx b/packages/vulcan-ui-material/components/forms/FormNestedDivider.jsx new file mode 100644 index 000000000..344ba4fdb --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormNestedDivider.jsx @@ -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 }) => ; + +FormNestedDivider.propTypes = { + classes: PropTypes.object.isRequired, + label: PropTypes.string, + addItem: PropTypes.func, +}; + +registerComponent('FormNestedDivider', FormNestedDivider, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/forms/FormNestedFoot.jsx b/packages/vulcan-ui-material/components/forms/FormNestedFoot.jsx new file mode 100644 index 000000000..7cb6b98eb --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormNestedFoot.jsx @@ -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 }) => ( + + + + + +); + +FormNestedFoot.propTypes = { + label: PropTypes.string, + addItem: PropTypes.func, +}; + +registerComponent('FormNestedFoot', FormNestedFoot); diff --git a/packages/vulcan-ui-material/components/forms/FormNestedHead.jsx b/packages/vulcan-ui-material/components/forms/FormNestedHead.jsx new file mode 100644 index 000000000..bd24a88d5 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormNestedHead.jsx @@ -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 }) => ; + +FormNestedHead.propTypes = { + label: PropTypes.string, + addItem: PropTypes.func, +}; + +replaceComponent('FormNestedHead', FormNestedHead); diff --git a/packages/vulcan-ui-material/components/forms/FormSubmit.jsx b/packages/vulcan-ui-material/components/forms/FormSubmit.jsx new file mode 100644 index 000000000..b8b92a301 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/FormSubmit.jsx @@ -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 ( +
+ + { + deleteDocument + ? + + + + + + : + null + } + + { + cancelCallback + ? + + : + null + } + + { + revertCallback + ? + + : + null + } + + + +
+ ); +}; + + +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]); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/EndAdornment.jsx b/packages/vulcan-ui-material/components/forms/base-controls/EndAdornment.jsx new file mode 100644 index 000000000..4f136be08 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/EndAdornment.jsx @@ -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 && + { + event.preventDefault(); + changeValue(null); + }} + tabIndex="-1" + > + + ; + + return ( + + {instantiateComponent(addonAfter)} + {clearButton} + + ); +}; + + +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); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiCheckboxGroup.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiCheckboxGroup.jsx new file mode 100644 index 000000000..4d80d8492 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiCheckboxGroup.jsx @@ -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 ( + 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 ( + + {controls} + + ); + }, + + render: function () { + + if (this.props.layout === 'elementOnly') { + return ( +
{this.renderElement()}
+ ); + } + + return ( + + {this.renderElement()} + + + ); + } +}); + + +export default withStyles(styles)(MuiCheckboxGroup); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiFormControl.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiFormControl.jsx new file mode 100644 index 000000000..7b426355d --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiFormControl.jsx @@ -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 ( + * + ); + }, + + renderLabel: function () { + if (this.props.layout === 'elementOnly' || this.props.hideLabel) { + return null; + } + + if (this.props.fakeLabel) { + return ( + + {this.props.label} + {this.renderRequiredSymbol()} + + ); + } + + const shrink = ['date', 'time', 'datetime'].includes(this.props.inputType) ? true : undefined; + + return ( + + {this.props.label} + {this.renderRequiredSymbol()} + + ); + }, + + render: function () { + const { layout, className, children, hasErrors } = this.props; + + if (layout === 'elementOnly') { + return {children}; + } + + return ( + + {this.renderLabel()} + {children} + + ); + } + +}); + + +export default MuiFormControl; diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiFormHelper.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiFormHelper.jsx new file mode 100644 index 000000000..58a5f4e5d --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiFormHelper.jsx @@ -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 && + ; + + return ( + + + + { + hasErrors ? errorMessage : help + } + + + { + showCharsRemaining && + + + {charsCount} / {max} + + } + + + ); +}; + + +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); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiInput.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiInput.jsx new file mode 100644 index 000000000..0f5775c4f --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiInput.jsx @@ -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 : + ; + const endAdornment = + ; + + let element = this.renderElement(startAdornment, endAdornment); + + if (this.props.layout === 'elementOnly' || this.props.type === 'hidden') { + return element; + } + + return ( + + {element} + + + ); + }, + + 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 ( + (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); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiRadioGroup.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiRadioGroup.jsx new file mode 100644 index 000000000..d111dd111 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiRadioGroup.jsx @@ -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 ( + 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 ( + + {controls} + + ); + }, + + render: function () { + + if (this.props.layout === 'elementOnly') { + return ( +
{this.renderElement()}
+ ); + } + + return ( + + {this.renderElement()} + + + ); + } +}); + + +export default withStyles(styles)(MuiRadioGroup); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiSelect.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiSelect.jsx new file mode 100644 index 000000000..6c50f1bbd --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiSelect.jsx @@ -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 ( + + {this.renderElement()} + + + ); + }, + + renderElement: function () { + const renderOption = (item, key) => { + //eslint-disable-next-line no-unused-vars + const { group, label, ...rest } = item; + return this.props.native + ? + + : + {label}; + }; + + const renderGroup = (label, key, nodes) => { + return this.props.native + ? + + {nodes} + + : + {label}} key={key}> + {nodes} + ; + }; + + 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 : + ; + const endAdornment = + ; + + return ( + } + > + {optionNodes} + + ); + } +}); + + +export default withStyles(styles)(MuiSelect); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiSuggest.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiSuggest.jsx new file mode 100644 index 000000000..287491b60 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiSuggest.jsx @@ -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 : + ; + const endAdornment = + ; + + const element = this.renderElement(startAdornment, endAdornment); + + if (this.props.layout === 'elementOnly') { + return element; + } + + return ( + + {element} + + + ); + }, + + renderElement: function (startAdornment, endAdornment) { + const { classes, autoFocus, disableText, showAllOptions } = this.props; + + return ( + + ); + }, + + renderInputComponent: function (inputProps) { + const { classes, autoFocus, autoComplete, value, ref, startAdornment, endAdornment, disabled, ...rest } = inputProps; + + return ( + { 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 ( + + { + suggestion.iconComponent && +
+ {suggestion.iconComponent} +
+ } +
+ {parts.map((part, index) => { + return part.highlight ? ( + + {part.text} + + ) : ( + + {part.text} + + ); + })} +
+
+ ); + }, + + renderSuggestionsContainer: function ({ containerProps, children }) { + const { classes } = this.props; + + return ( + + + {children} + + + ); + }, + + 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]); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/MuiSwitch.jsx b/packages/vulcan-ui-material/components/forms/base-controls/MuiSwitch.jsx new file mode 100644 index 000000000..52181f865 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/MuiSwitch.jsx @@ -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 ( + + {element} + + + ); + }, + + renderElement: function () { + return ( + 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; diff --git a/packages/vulcan-ui-material/components/forms/base-controls/StartAdornment.jsx b/packages/vulcan-ui-material/components/forms/base-controls/StartAdornment.jsx new file mode 100644 index 000000000..58d6a7434 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/StartAdornment.jsx @@ -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' && + + + ; + + + return ( + + {instantiateComponent(addonBefore)} + {urlButton} + + ); +}; + + +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); diff --git a/packages/vulcan-ui-material/components/forms/base-controls/mixins/component.jsx b/packages/vulcan-ui-material/components/forms/base-controls/mixins/component.jsx new file mode 100644 index 000000000..7913cbfc5 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/base-controls/mixins/component.jsx @@ -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); + }, + +}; diff --git a/packages/vulcan-ui-material/components/forms/controls/Checkbox.jsx b/packages/vulcan-ui-material/components/forms/controls/Checkbox.jsx new file mode 100644 index 000000000..4f016d1ad --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Checkbox.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MuiSwitch from '../base-controls/MuiSwitch'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const CheckboxComponent = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentCheckbox', CheckboxComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/CheckboxGroup.jsx b/packages/vulcan-ui-material/components/forms/controls/CheckboxGroup.jsx new file mode 100644 index 000000000..226693354 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/CheckboxGroup.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MuiCheckboxGroup from '../base-controls/MuiCheckboxGroup'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const CheckboxGroupComponent = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentCheckboxGroup', CheckboxGroupComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/CountrySelect.jsx b/packages/vulcan-ui-material/components/forms/controls/CountrySelect.jsx new file mode 100644 index 000000000..1c4cf14be --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/CountrySelect.jsx @@ -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 }) => + ; + + +registerComponent('CountrySelect', CountrySelect); diff --git a/packages/vulcan-ui-material/components/forms/controls/Date.jsx b/packages/vulcan-ui-material/components/forms/controls/Date.jsx new file mode 100644 index 000000000..190e08ced --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Date.jsx @@ -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 }) => + ; + + +registerComponent('FormComponentDate', DateComponent, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/forms/controls/DateRdt.jsx b/packages/vulcan-ui-material/components/forms/controls/DateRdt.jsx new file mode 100644 index 000000000..f012d6717 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/DateRdt.jsx @@ -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 ( +
+ +
+ this.updateDate(newDate)} + inputProps={{name: this.props.name}} + /> +
+
+ ); + } +} + +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; diff --git a/packages/vulcan-ui-material/components/forms/controls/DateTime.jsx b/packages/vulcan-ui-material/components/forms/controls/DateTime.jsx new file mode 100644 index 000000000..5fc870a0a --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/DateTime.jsx @@ -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 }) => + ; + + +registerComponent('FormComponentDateTime', DateTimeComponent, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/forms/controls/DateTimeRdt.jsx b/packages/vulcan-ui-material/components/forms/controls/DateTimeRdt.jsx new file mode 100644 index 000000000..21c308d9e --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/DateTimeRdt.jsx @@ -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 ( +
+ +
+ this.updateDate(newDate._d)} + format={"x"} + inputProps={{name: this.props.name}} + /> +
+
+ ); + } +} + +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; diff --git a/packages/vulcan-ui-material/components/forms/controls/Default.jsx b/packages/vulcan-ui-material/components/forms/controls/Default.jsx new file mode 100644 index 000000000..e35f64abd --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Default.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MuiInput from '../base-controls/MuiInput'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const Default = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentDefault', Default); diff --git a/packages/vulcan-ui-material/components/forms/controls/Email.jsx b/packages/vulcan-ui-material/components/forms/controls/Email.jsx new file mode 100644 index 000000000..f4688361f --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Email.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MuiInput from '../base-controls/MuiInput'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const EmailComponent = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentEmail', EmailComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/Number.jsx b/packages/vulcan-ui-material/components/forms/controls/Number.jsx new file mode 100644 index 000000000..03136d0c3 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Number.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MuiInput from '../base-controls/MuiInput'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const NumberComponent = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentNumber', NumberComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/PostalCode.jsx b/packages/vulcan-ui-material/components/forms/controls/PostalCode.jsx new file mode 100644 index 000000000..810433e6c --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/PostalCode.jsx @@ -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 ; +}; + + +registerComponent('PostalCode', PostalCode); diff --git a/packages/vulcan-ui-material/components/forms/controls/RadioGroup.jsx b/packages/vulcan-ui-material/components/forms/controls/RadioGroup.jsx new file mode 100644 index 000000000..63d0052e9 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/RadioGroup.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MuiRadioGroup from '../base-controls/MuiRadioGroup'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const RadioGroupComponent = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentRadioGroup', RadioGroupComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/RegionSelect.jsx b/packages/vulcan-ui-material/components/forms/controls/RegionSelect.jsx new file mode 100644 index 000000000..a790bb351 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/RegionSelect.jsx @@ -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 ; + } else { + return ; + } +}; + + +registerComponent('RegionSelect', RegionSelect); diff --git a/packages/vulcan-ui-material/components/forms/controls/Select.jsx b/packages/vulcan-ui-material/components/forms/controls/Select.jsx new file mode 100644 index 000000000..7ff92e343 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Select.jsx @@ -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 ; +}; + + +registerComponent('FormComponentSelect', SelectComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/SelectMultiple.jsx b/packages/vulcan-ui-material/components/forms/controls/SelectMultiple.jsx new file mode 100644 index 000000000..5b3f552ce --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/SelectMultiple.jsx @@ -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 ; +}; + + +registerComponent('FormComponentSelectMultiple', SelectMultiple); diff --git a/packages/vulcan-ui-material/components/forms/controls/Textarea.jsx b/packages/vulcan-ui-material/components/forms/controls/Textarea.jsx new file mode 100644 index 000000000..988960741 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Textarea.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import MuiInput from '../base-controls/MuiInput'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const TextareaComponent = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentTextarea', TextareaComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/Time.jsx b/packages/vulcan-ui-material/components/forms/controls/Time.jsx new file mode 100644 index 000000000..e52bb3cbb --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Time.jsx @@ -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 }) => + ; + + +registerComponent('FormComponentTime', TimeComponent, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/forms/controls/TimeRdt.jsx b/packages/vulcan-ui-material/components/forms/controls/TimeRdt.jsx new file mode 100644 index 000000000..cf8334241 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/TimeRdt.jsx @@ -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 ( +
+ +
+ this.updateDate(newDate)} + inputProps={{name: this.props.name}} + /> +
+
+ ); + } +} + +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; diff --git a/packages/vulcan-ui-material/components/forms/controls/Url.jsx b/packages/vulcan-ui-material/components/forms/controls/Url.jsx new file mode 100644 index 000000000..d5d9ebf13 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/Url.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MuiInput from '../base-controls/MuiInput'; +import { registerComponent } from 'meteor/vulcan:core'; + + +const UrlComponent = ({ refFunction, ...properties }) => + ; + + +registerComponent('FormComponentUrl', UrlComponent); diff --git a/packages/vulcan-ui-material/components/forms/controls/countries.js b/packages/vulcan-ui-material/components/forms/controls/countries.js new file mode 100644 index 000000000..a2d84b1e7 --- /dev/null +++ b/packages/vulcan-ui-material/components/forms/controls/countries.js @@ -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; + } +}; diff --git a/packages/vulcan-ui-material/components/index.js b/packages/vulcan-ui-material/components/index.js new file mode 100644 index 000000000..e619680ee --- /dev/null +++ b/packages/vulcan-ui-material/components/index.js @@ -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'; diff --git a/packages/vulcan-ui-material/components/theme/JssCleanup.jsx b/packages/vulcan-ui-material/components/theme/JssCleanup.jsx new file mode 100644 index 000000000..4d344294b --- /dev/null +++ b/packages/vulcan-ui-material/components/theme/JssCleanup.jsx @@ -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; diff --git a/packages/vulcan-ui-material/components/theme/ThemeStyles.jsx b/packages/vulcan-ui-material/components/theme/ThemeStyles.jsx new file mode 100644 index 000000000..7441fc01b --- /dev/null +++ b/packages/vulcan-ui-material/components/theme/ThemeStyles.jsx @@ -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 =
{colorName}
; + } + + let rowStyle = { + backgroundColor: bgColor, + color: fgColor, + listStyle: 'none', + padding: 15, + }; + + if (colorValue.toString().indexOf('A1') === 0) { + rowStyle = { + ...rowStyle, + marginTop: 4, + }; + } + + return ( +
  • + {blockTitle} +
    + {colorValue} + {bgColor.toUpperCase()} +
    +
  • + ); +} + +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 ( +
      + {getColorBlock(theme, classes, cssColor, 500, true)} +
      + {colorsList} +
    + ); +} + + +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 ( + + + + + h1: {describeTypography(theme, 'h1')} + + + h2: {describeTypography(theme, 'h2')} + + + h3: {describeTypography(theme, 'h3')} + + + h4: {describeTypography(theme, 'h4')} + + + + + + + Headline: {describeTypography(theme, 'h5')} + + + Title: {describeTypography(theme, 'h6')} + + + Subtitle1: {describeTypography(theme, 'subtitle1')} + + + Body 1: {describeTypography(theme, 'body1')} - {latin} + + + {latin} + + + Body 2: {describeTypography(theme, 'body2')} - {latin} + + + {latin} + + + Caption: {describeTypography(theme, 'caption')} + + + Base: {describeTypography(theme)} - {latin} + + + Button - {describeTypography(theme)} + + + + + + { + getColorGroup({ + theme, + classes, + color: 'primary', + showAltPalette: true, + }) + } + + + + { + getColorGroup({ + theme, + classes, + color: 'secondary', + showAltPalette: true, + }) + } + + + + { + getColorGroup({ + theme, + classes, + color: 'error', + showAltPalette: true, + }) + } + + + + { + getColorGroup({ + theme, + classes, + color: 'background', + showAltPalette: true, + }) + } + + + + ); +}; + + +ThemeStyles.propTypes = { + theme: PropTypes.object.isRequired, + classes: PropTypes.object.isRequired, +}; + + +registerComponent('ThemeStyles', ThemeStyles, [withTheme, null], [withStyles, styles]); diff --git a/packages/vulcan-ui-material/components/ui/Alert.jsx b/packages/vulcan-ui-material/components/ui/Alert.jsx new file mode 100644 index 000000000..7a1254f5d --- /dev/null +++ b/packages/vulcan-ui-material/components/ui/Alert.jsx @@ -0,0 +1,31 @@ +/** + * @Author: Apollinaire Lecocq + * @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 }) => ( + + {children} + +); + +registerComponent({ name: 'Alert', component: Alert, hocs: [[withStyles, AlertStyle]] }); diff --git a/packages/vulcan-ui-material/components/ui/Button.jsx b/packages/vulcan-ui-material/components/ui/Button.jsx new file mode 100644 index 000000000..23ea96d84 --- /dev/null +++ b/packages/vulcan-ui-material/components/ui/Button.jsx @@ -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 ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; + + +registerComponent('Button', Button); diff --git a/packages/vulcan-ui-material/components/upload/UploadImage.jsx b/packages/vulcan-ui-material/components/upload/UploadImage.jsx new file mode 100755 index 000000000..6904e2fba --- /dev/null +++ b/packages/vulcan-ui-material/components/upload/UploadImage.jsx @@ -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 ( +
    + +
    + + + { + loading && + +
    + +
    + } +
    + + + + + +
    + ); + } +} + + +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]); diff --git a/packages/vulcan-ui-material/components/upload/UploadInner.jsx b/packages/vulcan-ui-material/components/upload/UploadInner.jsx new file mode 100755 index 000000000..b3c4ab007 --- /dev/null +++ b/packages/vulcan-ui-material/components/upload/UploadInner.jsx @@ -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 ( + + + + {label} + + { + help && + + {help} + } +
    + { + (disabled && !enableMultiple) + ? + null + : + +
    + +
    + {uploading && ( +
    + + + +
    + )} +
    + } + + {!!images.length && ( +
    +
    + {images.map( + (image, index) => + !isDeleted(index) && ( + + ) + )} +
    +
    + )} +
    + +
    + ); +}; + + +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]); diff --git a/packages/vulcan-ui-material/en_US.js b/packages/vulcan-ui-material/en_US.js new file mode 100644 index 000000000..57fb41cec --- /dev/null +++ b/packages/vulcan-ui-material/en_US.js @@ -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}}", + +}); diff --git a/packages/vulcan-ui-material/example/Header.jsx b/packages/vulcan-ui-material/example/Header.jsx new file mode 100644 index 000000000..7ae557a4f --- /dev/null +++ b/packages/vulcan-ui-material/example/Header.jsx @@ -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 ( + + + + toggleSideNav()} + className={classNames(classes.menuButton)} + color="inherit" + > + {isSideNavOpen ? : } + + +
    + + + {siteTitle} + + +
    + +
    +
    + ); +}; + + +Header.propTypes = { + classes: PropTypes.object.isRequired, + isSideNavOpen: PropTypes.bool, + toggleSideNav: PropTypes.func, +}; + + +Header.displayName = 'Header'; + + +registerComponent('Header', Header, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/example/Layout.jsx b/packages/vulcan-ui-material/example/Layout.jsx new file mode 100644 index 000000000..3d6d52885 --- /dev/null +++ b/packages/vulcan-ui-material/example/Layout.jsx @@ -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 ( +
    +
    + + + this.toggle('sideNav', openOrClose)} /> + + + + + + + + + +
    + {this.props.children} +
    + + + +
    +
    + ); + }; +} + + +Layout.propTypes = { + classes: PropTypes.object.isRequired, + children: PropTypes.node, +}; + + +Layout.displayName = 'Layout'; + + +replaceComponent('Layout', Layout, [withStyles, styles]); diff --git a/packages/vulcan-ui-material/example/SideNavigation.jsx b/packages/vulcan-ui-material/example/SideNavigation.jsx new file mode 100644 index 000000000..2d30e417f --- /dev/null +++ b/packages/vulcan-ui-material/example/SideNavigation.jsx @@ -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 ( +
    + + + {this.props.history.push('/');}}> + + + + + + + + { + Users.isAdmin(currentUser) && + +
    + + + this.toggle('admin')}> + + + + + {isOpen.admin ? : } + + + {this.props.history.push('/admin');}}> + + + + + + {this.props.history.push('/theme');}}> + + + + + + + +
    + } + +
    + ); + } +} + + +SideNavigation.propTypes = { + classes: PropTypes.object.isRequired, + currentUser: PropTypes.object, +}; + + +SideNavigation.displayName = 'SideNavigation'; + + +registerComponent('SideNavigation', SideNavigation, [withStyles, styles], withCurrentUser, withRouter); diff --git a/packages/vulcan-ui-material/forms.css b/packages/vulcan-ui-material/forms.css new file mode 100644 index 000000000..219fee574 --- /dev/null +++ b/packages/vulcan-ui-material/forms.css @@ -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; +} diff --git a/packages/vulcan-ui-material/fr_FR.js b/packages/vulcan-ui-material/fr_FR.js new file mode 100644 index 000000000..2d5c04745 --- /dev/null +++ b/packages/vulcan-ui-material/fr_FR.js @@ -0,0 +1,8 @@ +import { addStrings } from 'meteor/vulcan:core'; + +addStrings('fr', { + + "search.search": "Recherche", + "search.clear": "Effacer la recherche", + +}); diff --git a/packages/vulcan-ui-material/history.md b/packages/vulcan-ui-material/history.md new file mode 100644 index 000000000..8613491ff --- /dev/null +++ b/packages/vulcan-ui-material/history.md @@ -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 diff --git a/packages/vulcan-ui-material/modules/index.js b/packages/vulcan-ui-material/modules/index.js new file mode 100644 index 000000000..5a65d1ab1 --- /dev/null +++ b/packages/vulcan-ui-material/modules/index.js @@ -0,0 +1,4 @@ +export * from './themes'; +export JssCleanup from '../components/theme/JssCleanup'; +import './sampleTheme'; +import './routes'; diff --git a/packages/vulcan-ui-material/modules/routes.js b/packages/vulcan-ui-material/modules/routes.js new file mode 100755 index 000000000..ee9c6cf06 --- /dev/null +++ b/packages/vulcan-ui-material/modules/routes.js @@ -0,0 +1,8 @@ +import { addRoute } from 'meteor/vulcan:core'; + + +addRoute({ + name: 'theme', + path: '/theme', + componentName: 'ThemeStyles', +}); diff --git a/packages/vulcan-ui-material/modules/sampleTheme.js b/packages/vulcan-ui-material/modules/sampleTheme.js new file mode 100644 index 000000000..a382078a7 --- /dev/null +++ b/packages/vulcan-ui-material/modules/sampleTheme.js @@ -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); diff --git a/packages/vulcan-ui-material/modules/themes.js b/packages/vulcan-ui-material/modules/themes.js new file mode 100644 index 000000000..eb9b57744 --- /dev/null +++ b/packages/vulcan-ui-material/modules/themes.js @@ -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; +}; diff --git a/packages/vulcan-ui-material/package.js b/packages/vulcan-ui-material/package.js new file mode 100644 index 000000000..024850c42 --- /dev/null +++ b/packages/vulcan-ui-material/package.js @@ -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'); +}); diff --git a/packages/vulcan-ui-material/readme.md b/packages/vulcan-ui-material/readme.md new file mode 100644 index 000000000..05f71b4b4 --- /dev/null +++ b/packages/vulcan-ui-material/readme.md @@ -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: , // adorn the start of the input + addonAfter: , // 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 } + onClick={scheduleAgent}/>; +}; + +AgendaJobActionsInner.propTypes = { + collecion: PropTypes.object.isRequired, + document: PropTypes.object.isRequired, +}; + + +``` + +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'; + + + {getCountryLabel(supplier.country)} + +``` + diff --git a/packages/vulcan-ui-material/server/main.js b/packages/vulcan-ui-material/server/main.js new file mode 100644 index 000000000..1ed6f2fb0 --- /dev/null +++ b/packages/vulcan-ui-material/server/main.js @@ -0,0 +1,3 @@ +export * from '../components/index'; +export * from '../modules/index'; +import './wrapWithMuiTheme'; diff --git a/packages/vulcan-ui-material/server/wrapWithMuiTheme.jsx b/packages/vulcan-ui-material/server/wrapWithMuiTheme.jsx new file mode 100644 index 000000000..32933c0e5 --- /dev/null +++ b/packages/vulcan-ui-material/server/wrapWithMuiTheme.jsx @@ -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 ( + + + + {app} + + + + ); +} + + +function injectJss(sink, { context }) { + const sheets = context.sheetsRegistry.toString(); + sink.appendToHead( + `` + ); + return sink; +} + + +addCallback('router.server.wrapper', wrapWithMuiTheme); +addCallback('router.server.postRender', injectJss);