diff options
Diffstat (limited to 'frontend/src/components/fields')
-rw-r--r-- | frontend/src/components/fields/ButtonField.jsx | 78 | ||||
-rw-r--r-- | frontend/src/components/fields/CheckField.jsx | 67 | ||||
-rw-r--r-- | frontend/src/components/fields/ChoiceField.jsx | 85 | ||||
-rw-r--r-- | frontend/src/components/fields/InputField.jsx | 95 | ||||
-rw-r--r-- | frontend/src/components/fields/TagField.jsx | 75 | ||||
-rw-r--r-- | frontend/src/components/fields/TextField.jsx | 60 | ||||
-rw-r--r-- | frontend/src/components/fields/extensions/ColorField.jsx | 98 | ||||
-rw-r--r-- | frontend/src/components/fields/extensions/NumericField.jsx | 62 |
8 files changed, 620 insertions, 0 deletions
diff --git a/frontend/src/components/fields/ButtonField.jsx b/frontend/src/components/fields/ButtonField.jsx new file mode 100644 index 0000000..ae5b33a --- /dev/null +++ b/frontend/src/components/fields/ButtonField.jsx @@ -0,0 +1,78 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, { Component } from "react"; +import "./ButtonField.scss"; +import "./common.scss"; +import classNames from "classnames"; + +class ButtonField extends Component { + render() { + const handler = () => { + if (typeof this.props.onClick === "function") { + this.props.onClick(); + } + }; + + let buttonClassnames = { + "button-bordered": this.props.bordered, + }; + if (this.props.variant) { + buttonClassnames[`button-variant-${this.props.variant}`] = true; + } + + let buttonStyle = {}; + if (this.props.color) { + buttonStyle["backgroundColor"] = this.props.color; + } + if (this.props.border) { + buttonStyle["borderColor"] = this.props.border; + } + if (this.props.fullSpan) { + buttonStyle["width"] = "100%"; + } + if (this.props.rounded) { + buttonStyle["borderRadius"] = "3px"; + } + if (this.props.inline) { + buttonStyle["marginTop"] = "8px"; + } + + return ( + <div + className={classNames( + "field", + "button-field", + { "field-small": this.props.small }, + { "field-active": this.props.active } + )} + > + <button + type="button" + className={classNames(buttonClassnames)} + onClick={handler} + style={buttonStyle} + disabled={this.props.disabled} + > + {this.props.name} + </button> + </div> + ); + } +} + +export default ButtonField; diff --git a/frontend/src/components/fields/CheckField.jsx b/frontend/src/components/fields/CheckField.jsx new file mode 100644 index 0000000..1a04f18 --- /dev/null +++ b/frontend/src/components/fields/CheckField.jsx @@ -0,0 +1,67 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, { Component } from "react"; +import { randomClassName } from "../../utils"; +import "./CheckField.scss"; +import "./common.scss"; + +import classNames from "classnames"; + +class CheckField extends Component { + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const checked = this.props.checked || false; + const small = this.props.small || false; + const name = this.props.name || null; + const handler = () => { + if (!this.props.readonly && this.props.onChange) { + this.props.onChange(!checked); + } + }; + + return ( + <div + className={classNames( + "field", + "check-field", + { "field-checked": checked }, + { "field-small": small } + )} + > + <div className="field-input"> + <input + type="checkbox" + id={this.id} + checked={checked} + onChange={handler} + /> + <label htmlFor={this.id}> + {(checked ? "✓ " : "✗ ") + (name != null ? name : "")} + </label> + </div> + </div> + ); + } +} + +export default CheckField; diff --git a/frontend/src/components/fields/ChoiceField.jsx b/frontend/src/components/fields/ChoiceField.jsx new file mode 100644 index 0000000..c44d0a9 --- /dev/null +++ b/frontend/src/components/fields/ChoiceField.jsx @@ -0,0 +1,85 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import {randomClassName} from "../../utils"; +import "./ChoiceField.scss"; +import "./common.scss"; + +import classNames from 'classnames'; + +class ChoiceField extends Component { + + constructor(props) { + super(props); + + this.state = { + expanded: false + }; + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const name = this.props.name || null; + const inline = this.props.inline; + + const collapse = () => this.setState({expanded: false}); + const expand = () => this.setState({expanded: true}); + + const handler = (key) => { + collapse(); + if (this.props.onChange) { + this.props.onChange(key); + } + }; + + const keys = this.props.keys || []; + const values = this.props.values || []; + + const options = keys.map((key, i) => + <span className="field-option" key={key} onClick={() => handler(key)}>{values[i]}</span> + ); + + let fieldValue = ""; + if (inline && name) { + fieldValue = name; + } + if (!this.props.onlyName && inline && name) { + fieldValue += ": "; + } + if (!this.props.onlyName) { + fieldValue += this.props.value || "select a value"; + } + + return ( + <div className={classNames("field", "choice-field", {"field-inline": inline}, + {"field-small": this.props.small})}> + {!inline && name && <label className="field-name">{name}:</label>} + <div className={classNames("field-select", {"select-expanded": this.state.expanded})} + tabIndex={0} onBlur={collapse} onClick={() => this.state.expanded ? collapse() : expand()}> + <div className="field-value">{fieldValue}</div> + <div className="field-options"> + {options} + </div> + </div> + </div> + ); + } +} + +export default ChoiceField; diff --git a/frontend/src/components/fields/InputField.jsx b/frontend/src/components/fields/InputField.jsx new file mode 100644 index 0000000..f9609df --- /dev/null +++ b/frontend/src/components/fields/InputField.jsx @@ -0,0 +1,95 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import {randomClassName} from "../../utils"; +import "./common.scss"; +import "./InputField.scss"; + +import classNames from 'classnames'; + +class InputField extends Component { + + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const active = this.props.active || false; + const invalid = this.props.invalid || false; + const small = this.props.small || false; + const inline = this.props.inline || false; + const name = this.props.name || null; + const value = this.props.value || ""; + const defaultValue = this.props.defaultValue || ""; + const type = this.props.type || "text"; + const error = this.props.error || null; + + const handler = (e) => { + if (typeof this.props.onChange === "function") { + if (type === "file") { + let file = e.target.files[0]; + this.props.onChange(file); + } else if (e == null) { + this.props.onChange(defaultValue); + } else { + this.props.onChange(e.target.value); + } + } + }; + let inputProps = {}; + if (type !== "file") { + inputProps["value"] = value || defaultValue; + } + + return ( + <div className={classNames("field", "input-field", {"field-active": active}, + {"field-invalid": invalid}, {"field-small": small}, {"field-inline": inline})}> + <div className="field-wrapper"> + {name && + <div className="field-name"> + <label>{name}:</label> + </div> + } + <div className="field-input"> + <div className="field-value"> + {type === "file" && <label for={this.id} className={"file-label"}> + {value.name || this.props.placeholder}</label>} + <input type={type} placeholder={this.props.placeholder} id={this.id} + aria-describedby={this.id} onChange={handler} {...inputProps} + readOnly={this.props.readonly}/> + </div> + {type !== "file" && value !== "" && !this.props.readonly && + <div className="field-clear"> + <span onClick={() => handler(null)}>del</span> + </div> + } + </div> + </div> + {error && + <div className="field-error"> + error: {error} + </div> + } + </div> + ); + } +} + +export default InputField; diff --git a/frontend/src/components/fields/TagField.jsx b/frontend/src/components/fields/TagField.jsx new file mode 100644 index 0000000..79e2314 --- /dev/null +++ b/frontend/src/components/fields/TagField.jsx @@ -0,0 +1,75 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import ReactTags from "react-tag-autocomplete"; +import {randomClassName} from "../../utils"; +import "./common.scss"; +import "./TagField.scss"; + +import classNames from 'classnames'; +import _ from 'lodash'; + +class TagField extends Component { + + state = {}; + + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + onAddition = (tag) => { + if (typeof this.props.onChange === "function") { + this.props.onChange([].concat(this.props.tags, tag), true, tag); // true == addition + } + }; + + onDelete = (i) => { + if (typeof this.props.onChange === "function") { + const tags = _.clone(this.props.tags); + const tag = tags[i]; + tags.splice(i, 1); + this.props.onChange(tags, true, tag); // false == delete + } + }; + + + render() { + const small = this.props.small || false; + const name = this.props.name || null; + + return ( + <div className={classNames("field", "tag-field", {"field-small": small}, + {"field-inline": this.props.inline})}> + {name && + <div className="field-name"> + <label>{name}:</label> + </div> + } + <div className="field-input"> + <ReactTags {...this.props} tags={this.props.tags || []} autoresize={false} + onDelete={this.onDelete} onAddition={this.onAddition} + placeholderText={this.props.placeholder || ""}/> + </div> + </div> + ); + } +} + +export default TagField; diff --git a/frontend/src/components/fields/TextField.jsx b/frontend/src/components/fields/TextField.jsx new file mode 100644 index 0000000..1138ffc --- /dev/null +++ b/frontend/src/components/fields/TextField.jsx @@ -0,0 +1,60 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import {randomClassName} from "../../utils"; +import "./common.scss"; +import "./TextField.scss"; + +import classNames from 'classnames'; + +class TextField extends Component { + + constructor(props) { + super(props); + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const name = this.props.name || null; + const error = this.props.error || null; + const rows = this.props.rows || 3; + + const handler = (e) => { + if (this.props.onChange) { + if (e == null) { + this.props.onChange(""); + } else { + this.props.onChange(e.target.value); + } + } + }; + + return ( + <div className={classNames("field", "text-field", {"field-active": this.props.active}, + {"field-invalid": this.props.invalid}, {"field-small": this.props.small})}> + {name && <label htmlFor={this.id}>{name}:</label>} + <textarea id={this.id} placeholder={this.props.defaultValue} onChange={handler} rows={rows} + readOnly={this.props.readonly} value={this.props.value} ref={this.props.textRef}/> + {error && <div className="field-error">error: {error}</div>} + </div> + ); + } +} + +export default TextField; diff --git a/frontend/src/components/fields/extensions/ColorField.jsx b/frontend/src/components/fields/extensions/ColorField.jsx new file mode 100644 index 0000000..fd30988 --- /dev/null +++ b/frontend/src/components/fields/extensions/ColorField.jsx @@ -0,0 +1,98 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import {OverlayTrigger, Popover} from "react-bootstrap"; +import validation from "../../../validation"; +import InputField from "../InputField"; +import "./ColorField.scss"; + +class ColorField extends Component { + + constructor(props) { + super(props); + + this.state = {}; + + this.colors = ["#e53935", "#d81b60", "#8e24aa", "#5e35b1", "#3949ab", "#1e88e5", "#039be5", "#00acc1", + "#00897b", "#43a047", "#7cb342", "#9e9d24", "#f9a825", "#fb8c00", "#f4511e", "#6d4c41"]; + } + + componentDidUpdate(prevProps, prevState, snapshot) { + if (prevProps.value !== this.props.value) { + this.onChange(this.props.value); + } + } + + onChange = (value) => { + this.setState({invalid: value !== "" && !validation.isValidColor(value)}); + + if (typeof this.props.onChange === "function") { + this.props.onChange(value); + } + }; + + render() { + const colorButtons = this.colors.map((color) => + <span key={"button" + color} className="color-input" style={{"backgroundColor": color}} + onClick={() => { + if (typeof this.props.onChange === "function") { + this.props.onChange(color); + } + document.body.click(); // magic to close popup + }}/>); + + const popover = ( + <Popover id="popover-basic"> + <Popover.Title as="h3">choose a color</Popover.Title> + <Popover.Content> + <div className="colors-container"> + <div className="colors-row"> + {colorButtons.slice(0, 8)} + </div> + <div className="colors-row"> + {colorButtons.slice(8, 18)} + </div> + </div> + </Popover.Content> + </Popover> + ); + + let buttonStyles = {}; + if (this.props.value) { + buttonStyles["backgroundColor"] = this.props.value; + } + + return ( + <div className="field color-field"> + <div className="color-input"> + <InputField {...this.props} onChange={this.onChange} invalid={this.state.invalid} name="color" + error={null}/> + <div className="color-picker"> + <OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose> + <button type="button" className="picker-button" style={buttonStyles}>pick</button> + </OverlayTrigger> + </div> + </div> + {this.props.error && <div className="color-error">{this.props.error}</div>} + </div> + ); + } + +} + +export default ColorField; diff --git a/frontend/src/components/fields/extensions/NumericField.jsx b/frontend/src/components/fields/extensions/NumericField.jsx new file mode 100644 index 0000000..a6cba26 --- /dev/null +++ b/frontend/src/components/fields/extensions/NumericField.jsx @@ -0,0 +1,62 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import InputField from "../InputField"; + +class NumericField extends Component { + + constructor(props) { + super(props); + + this.state = { + invalid: false + }; + } + + componentDidUpdate(prevProps, prevState, snapshot) { + if (prevProps.value !== this.props.value) { + this.onChange(this.props.value); + } + } + + onChange = (value) => { + value = value.toString().replace(/[^\d]/gi, ""); + let intValue = 0; + if (value !== "") { + intValue = parseInt(value, 10); + } + const valid = + (!this.props.validate || (typeof this.props.validate === "function" && this.props.validate(intValue))) && + (!this.props.min || (typeof this.props.min === "number" && intValue >= this.props.min)) && + (!this.props.max || (typeof this.props.max === "number" && intValue <= this.props.max)); + this.setState({invalid: !valid}); + if (typeof this.props.onChange === "function") { + this.props.onChange(intValue); + } + }; + + render() { + return ( + <InputField {...this.props} onChange={this.onChange} defaultValue={this.props.defaultValue || "0"} + invalid={this.state.invalid}/> + ); + } + +} + +export default NumericField; |