diff --git a/packages/core/admin/admin/src/content-manager/components/InputJSON/FieldError.js b/packages/core/admin/admin/src/content-manager/components/InputJSON/FieldError.js new file mode 100644 index 0000000000..f921d478e7 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/InputJSON/FieldError.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; +import { P } from '@strapi/parts/Text'; + +export const FieldError = ({ id, error, name }) => { + const { formatMessage } = useIntl(); + const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : ''; + + if (!error) { + return null; + } + + return ( +

+ {errorMessage} +

+ ); +}; + +FieldError.defaultProps = { + id: undefined, + error: undefined, +}; + +FieldError.propTypes = { + error: PropTypes.string, + id: PropTypes.string, + name: PropTypes.string.isRequired, +}; + +export default FieldError; diff --git a/packages/core/admin/admin/src/content-manager/components/InputJSON/Hint.js b/packages/core/admin/admin/src/content-manager/components/InputJSON/Hint.js new file mode 100644 index 0000000000..693a3b1617 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/InputJSON/Hint.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; +import { P } from '@strapi/parts/Text'; + +export const Hint = ({ id, error, name, description }) => { + const { formatMessage } = useIntl(); + const hint = description + ? formatMessage( + { id: description.id, defaultMessage: description.defaultMessage }, + { ...description.values } + ) + : ''; + + if (!hint || error) { + return null; + } + + return ( +

+ {hint} +

+ ); +}; + +Hint.defaultProps = { + id: undefined, + description: undefined, + error: undefined, +}; + +Hint.propTypes = { + description: PropTypes.shape({ + id: PropTypes.string.isRequired, + defaultMessage: PropTypes.string.isRequired, + values: PropTypes.object, + }), + error: PropTypes.string, + id: PropTypes.string, + name: PropTypes.string.isRequired, +}; + +export default Hint; diff --git a/packages/core/admin/admin/src/content-manager/components/InputJSON/Label.js b/packages/core/admin/admin/src/content-manager/components/InputJSON/Label.js new file mode 100644 index 0000000000..ca79857525 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/InputJSON/Label.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { useIntl } from 'react-intl'; +import { Text } from '@strapi/parts/Text'; +import { Box } from '@strapi/parts/Box'; +import { Row } from '@strapi/parts/Row'; + +const LabelAction = styled(Box)` + svg path { + fill: ${({ theme }) => theme.colors.neutral500}; + } +`; + +const Label = ({ id, intlLabel, labelAction, name }) => { + const { formatMessage } = useIntl(); + const label = intlLabel?.id + ? formatMessage( + { id: intlLabel.id, defaultMessage: intlLabel.defaultMessage }, + { ...intlLabel.values } + ) + : name; + + return ( + + + {label} + + {labelAction && {labelAction}} + + ); +}; + +Label.defaultProps = { + id: undefined, + intlLabel: undefined, + labelAction: undefined, +}; + +Label.propTypes = { + id: PropTypes.string, + intlLabel: PropTypes.shape({ + id: PropTypes.string.isRequired, + defaultMessage: PropTypes.string.isRequired, + values: PropTypes.object, + }), + labelAction: PropTypes.element, + name: PropTypes.string.isRequired, +}; + +export default Label; diff --git a/packages/core/admin/admin/src/content-manager/components/InputJSON/components.js b/packages/core/admin/admin/src/content-manager/components/InputJSON/components.js index 3bf43697e1..60e22d26f7 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputJSON/components.js +++ b/packages/core/admin/admin/src/content-manager/components/InputJSON/components.js @@ -1,30 +1,314 @@ import styled from 'styled-components'; +import { Box } from '@strapi/parts/Box'; -const Wrapper = styled.div` - position: relative; - margin-bottom: 3px; - line-height: 18px; - cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'initial')}; - - .CodeMirror { - font-size: 13px !important; - } - - > div { - border-radius: 3px; - - > div:last-of-type { - min-height: 315px; - max-height: 635px; - font-weight: 500; - font-size: 1.3rem !important; - } - } +/* eslint-disable */ +/* stylelint-disable */ +const EditorWrapper = styled.div` + cursor: ${({ disabled }) => (disabled ? 'not-allowed !important' : 'auto')}; + /* BASICS */ .colored { background-color: yellow; color: black !important; } + + > div { + border-radius: 3px; + > div:last-of-type { + min-height: 253px; + max-height: 506px; + } + } + + .CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-size: ${14 / 16}rem; + color: ${({ theme }) => theme.colors.neutral800}; + direction: ltr; + } + + /* PADDING */ + + .CodeMirror-scrollbar-filler, + .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ + } + + /* GUTTER */ + + .CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; + } + .CodeMirror-linenumbers { + } + .CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; + } + + .CodeMirror-guttermarker { + color: black; + } + .CodeMirror-guttermarker-subtle { + color: #999; + } + + /* CURSOR */ + + .CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; + } + /* Shown when moving in bi-directional text */ + .CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; + } + .cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; + } + .cm-fat-cursor div.CodeMirror-cursors { + /* z-index: 1; */ + } + + .cm-fat-cursor-mark { + background-color: rgba(20, 255, 20, 0.5); + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + } + .cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; + } + + /* Can style cursor different in overwrite (non-insert) mode */ + .CodeMirror-overwrite .CodeMirror-cursor { + } + + .cm-tab { + display: inline-block; + text-decoration: inherit; + } + + .CodeMirror-rulers { + position: absolute; + left: 0; + right: 0; + top: -50px; + bottom: 0; + overflow: hidden; + } + .CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; + bottom: 0; + position: absolute; + } + + /* DEFAULT THEME */ + + .CodeMirror { + position: relative; + overflow: hidden; + background: white; + } + + .CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 50px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -50px; + margin-right: -50px; + padding-bottom: 50px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; + } + .CodeMirror-sizer { + position: relative; + border-right: 50px solid transparent; + } + + /* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ + .CodeMirror-vscrollbar, + .CodeMirror-hscrollbar, + .CodeMirror-scrollbar-filler, + .CodeMirror-gutter-filler { + position: absolute; + /* z-index: 6; */ + display: none; + outline: none; + } + + .CodeMirror-vscrollbar { + right: 0; + top: 0; + overflow-x: hidden; + overflow-y: scroll; + } + .CodeMirror-hscrollbar { + bottom: 0; + left: 0; + overflow-y: hidden; + overflow-x: scroll; + } + .CodeMirror-scrollbar-filler { + right: 0; + bottom: 0; + } + + .CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ + } + /* Reset some styles that the rest of the page might have set */ + .CodeMirror pre.CodeMirror-line, + .CodeMirror pre.CodeMirror-line-like { + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + /* z-index: 2; */ + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; + } + + .CodeMirror pre.CodeMirror-line-like { + z-index: 2; + } + + .CodeMirror-wrap pre.CodeMirror-line, + .CodeMirror-wrap pre.CodeMirror-line-like { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; + } + + .CodeMirror-linebackground { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 0; + } + + .CodeMirror-linewidget { + position: relative; + /* z-index: 2; */ + padding: 0.1px; /* Force widget margins to stay inside of the container */ + } + + .CodeMirror-widget { + } + + .CodeMirror-rtl pre { + direction: rtl; + } + + .CodeMirror-code { + outline: none; + } + + /* Force content-box sizing for the elements where we expect it */ + .CodeMirror-scroll, + .CodeMirror-sizer, + .CodeMirror-gutter, + .CodeMirror-gutters, + .CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; + } + + .CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; + } + + .CodeMirror-cursor { + position: absolute; + pointer-events: none; + } + .CodeMirror-measure pre { + position: static; + } + + div.CodeMirror-cursors { + visibility: hidden; + position: relative; + + div { + z-index: 0 !important; + } + } + + div.CodeMirror-dragcursors { + visibility: visible; + } + + .CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; + } + + .CodeMirror-selected { + background: ${({ theme }) => theme.colors.neutral200}; + /* z-index: -10; */ + } + .CodeMirror-crosshair { + cursor: crosshair; + } + + /* Used to force a border model for a node */ + .cm-force-border { + padding-right: 0.1px; + } + + /* See issue #2901 */ + .cm-tab-wrap-hack:after { + content: ''; + } + + /* Help users use markselection to safely style text background */ + span.CodeMirror-selectedtext { + background: none; + } + + .cm-s-solarized.CodeMirror { + box-shadow: none; + border-radius: ${({ theme }) => theme.borderRadius}; + } `; -export default Wrapper; +const StyledBox = styled(Box)` + border-radius: ${({ theme }) => theme.borderRadius}; + border: 1px solid ${({ theme, error }) => (error ? theme.colors.danger600 : 'transparent')}; +`; + +export { EditorWrapper, StyledBox }; diff --git a/packages/core/admin/admin/src/content-manager/components/InputJSON/index.js b/packages/core/admin/admin/src/content-manager/components/InputJSON/index.js index a3f849c2fd..b5053190fe 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputJSON/index.js +++ b/packages/core/admin/admin/src/content-manager/components/InputJSON/index.js @@ -7,10 +7,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import cm from 'codemirror'; - -import { trimStart } from 'lodash'; +import trimStart from 'lodash/trimStart'; +import { Stack } from '@strapi/parts/Stack'; import jsonlint from './jsonlint'; -import Wrapper from './components'; +import { EditorWrapper, StyledBox } from './components'; +import Hint from './Hint'; +import Label from './Label'; +import FieldError from './FieldError'; const WAIT = 600; const stringify = JSON.stringify; @@ -60,7 +63,6 @@ class InputJSON extends React.Component { fontSize: '13px', }); this.codeMirror.on('change', this.handleChange); - this.codeMirror.on('blur', this.handleBlur); this.setSize(); this.setInitValue(); @@ -112,21 +114,6 @@ class InputJSON extends React.Component { this.setState({ markedText }); }; - handleBlur = ({ target }) => { - const { name, onBlur } = this.props; - - if (target === undefined) { - // codemirror catches multiple events - onBlur({ - target: { - name, - type: 'json', - value: this.getValue(), - }, - }); - } - }; - handleChange = (doc, change) => { if (change.origin === 'setValue') { return; @@ -172,24 +159,61 @@ class InputJSON extends React.Component { } return ( - -