diff --git a/packages/components/package.json b/packages/components/package.json index 4b1366963b0acfd0e75f8cd3aefe328a57fff27e..c9c3193e27843611ab283c81c70eb7ca81313cae 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -18,6 +18,7 @@ "config": "^1.28.1", "css-loader": "^0.28.7", "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", "express": "^4.15.4", "file-loader": "^0.11.2", "font-awesome": "^4.7.0", @@ -30,8 +31,8 @@ "pubsweet-client": "^2.5.1", "pubsweet-server": "^7.0.1", "pubsweet-theme-plugin": "^0.0.3", - "react": "^16.2.0", - "react-dom": "^16.2.0", + "react": "^16.3.2", + "react-dom": "^16.3.2", "react-router-dom": "^4.2.2", "react-test-renderer": "^16.2.0", "redux": "^3.7.2", @@ -53,7 +54,7 @@ ], "testPathIgnorePatterns": [ "/node_modules", - "config/" + "config/test" ], "globals": { "PUBSWEET_COMPONENTS": [], diff --git a/packages/components/xpub-edit/package.json b/packages/components/xpub-edit/package.json index c0fe464735fa645a34552603aa0a4b70f7c3b8ff..0c0e746b99d3a88f720c80fc0ddb3ac28caa99fe 100644 --- a/packages/components/xpub-edit/package.json +++ b/packages/components/xpub-edit/package.json @@ -36,7 +36,7 @@ "xpub-styleguide": "^0.0.12" }, "peerDependencies": { - "react": ">=16" + "react": ">=16.3" }, "scripts": { "styleguide": "styleguidist server", diff --git a/packages/components/xpub-edit/src/components/Editor.js b/packages/components/xpub-edit/src/components/Editor.js index 2178628422c3325e2b8f3c123058ee528307496a..d65826a67aaedc23ccb3ef84ddaf6c93fb52a6d4 100644 --- a/packages/components/xpub-edit/src/components/Editor.js +++ b/packages/components/xpub-edit/src/components/Editor.js @@ -53,7 +53,18 @@ class Editor extends React.Component { } render() { - const { options, title, readonly } = this.props + const { + autoFocus, + basePlaceholderClassName, + className, + onBlur, + options, + placeholder, + placeholderClassName, + title, + readonly, + ...remainingProps + } = this.props const { state } = this.state const menu = readonly ? {} : options.menu @@ -68,7 +79,7 @@ class Editor extends React.Component { /> )} - <div ref={this.createEditorView} /> + <div ref={this.createEditorView} {...remainingProps} /> </div> ) } diff --git a/packages/components/xpub-edit/src/components/HtmlEditor.js b/packages/components/xpub-edit/src/components/HtmlEditor.js index 442dabd2f265d57d049211d993458ca9e391eb96..1913c77a488225eb90d889e3fd4f80e7c54c3204 100644 --- a/packages/components/xpub-edit/src/components/HtmlEditor.js +++ b/packages/components/xpub-edit/src/components/HtmlEditor.js @@ -48,27 +48,9 @@ class HtmlEditor extends React.Component { } render() { - const { - options, - className, - placeholder, - placeholderClassName, - title, - readonly, - } = this.props + const { value, onChange, onBlur, ...passedProps } = this.props - return ( - <Editor - className={className} - onBlur={this.onBlur} - onChange={this.onChange} - options={options} - placeholder={placeholder} - placeholderClassName={placeholderClassName} - readonly={readonly} - title={title} - /> - ) + return <Editor onChange={this.onChange} {...passedProps} /> } } diff --git a/packages/components/xpub-edit/src/components/HtmlViewer.js b/packages/components/xpub-edit/src/components/HtmlViewer.js index dbda30c6a759996c03607c9b84baca3fbbc6ce92..a898f435c0267732656375e4f798ea3fad57ad1b 100644 --- a/packages/components/xpub-edit/src/components/HtmlViewer.js +++ b/packages/components/xpub-edit/src/components/HtmlViewer.js @@ -24,9 +24,9 @@ class HtmlViewer extends React.Component { } render() { - const { options, className, value } = this.props + const { options, value, ...props } = this.props options.doc = this.changeContentValue(value) - return <Viewer className={className} options={options} /> + return <Viewer options={options} {...props} /> } } diff --git a/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.js b/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.js index 620a94edd923ef5bb19b399d14998a06a97e3817..2fd1b158ff5ef3ea065d8cb77ec141d871dc9975 100644 --- a/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.js +++ b/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.js @@ -1,29 +1,30 @@ import React from 'react' +import PropTypes from 'prop-types' import HtmlEditor from '../HtmlEditor' -import makeConfig from './config' +import EditorOptions from './EditorOptions' -const ConfigurableEditor = ({ - className, - value, - placeholder, - placeholderClassName, - title, - onBlur, - onChange, - readonly, - ...features -}) => ( - <HtmlEditor - className={className} - onBlur={onBlur} - onChange={onChange} - options={makeConfig(features)} - placeholder={placeholder} - placeholderClassName={placeholderClassName} - readonly={readonly} - title={title} - value={value} - /> +const ConfigurableEditor = props => ( + <EditorOptions {...props}> + {(options, passedProps) => ( + <HtmlEditor options={options} {...passedProps} /> + )} + </EditorOptions> ) +ConfigurableEditor.propTypes = { + bold: PropTypes.bool, + italic: PropTypes.bool, + underline: PropTypes.bool, + superscript: PropTypes.bool, + subscript: PropTypes.bool, + smallcaps: PropTypes.bool, + link: PropTypes.bool, + heading: PropTypes.bool, + undo: PropTypes.bool, + redo: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onBlur: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, +} + export default ConfigurableEditor diff --git a/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.md b/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.md index 08704cfa053d08edd2ea337e161841f9248ece4a..4aafd16cecc14c761a103e231cc1e3ade6d99e37 100644 --- a/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.md +++ b/packages/components/xpub-edit/src/components/configurable/ConfigurableEditor.md @@ -1,4 +1,4 @@ -An editor that can be configured simply with boolean props +An editor whose features can be configured with boolean props ```js const value = faker.lorem.sentence(20) diff --git a/packages/components/xpub-edit/src/components/configurable/ConfigurableViewer.js b/packages/components/xpub-edit/src/components/configurable/ConfigurableViewer.js index c9a46cfb9615d5ea245749c5dc7495c727344bc7..f1f52e17d6c16da0842c49bc03a4521b90710fbe 100644 --- a/packages/components/xpub-edit/src/components/configurable/ConfigurableViewer.js +++ b/packages/components/xpub-edit/src/components/configurable/ConfigurableViewer.js @@ -1,13 +1,27 @@ import React from 'react' +import PropTypes from 'prop-types' import HtmlViewer from '../HtmlViewer' -import makeConfig from './config' +import EditorOptions from './EditorOptions' -const ConfigurableViewer = ({ className, value, ...features }) => ( - <HtmlViewer - className={className} - options={makeConfig(features)} - value={value} - /> +const ConfigurableViewer = props => ( + <EditorOptions {...props}> + {(options, remainingProps) => ( + <HtmlViewer options={options} {...remainingProps} /> + )} + </EditorOptions> ) +ConfigurableViewer.propTypes = { + bold: PropTypes.bool, + italic: PropTypes.bool, + underline: PropTypes.bool, + superscript: PropTypes.bool, + subscript: PropTypes.bool, + smallcaps: PropTypes.bool, + link: PropTypes.bool, + heading: PropTypes.bool, + undo: PropTypes.bool, + redo: PropTypes.bool, +} + export default ConfigurableViewer diff --git a/packages/components/xpub-edit/src/components/configurable/ConfigurableViewer.md b/packages/components/xpub-edit/src/components/configurable/ConfigurableViewer.md new file mode 100644 index 0000000000000000000000000000000000000000..7cd17ff36b916b0f2218edcbc14cdea65cd19db6 --- /dev/null +++ b/packages/components/xpub-edit/src/components/configurable/ConfigurableViewer.md @@ -0,0 +1,6 @@ +A viewer whose features can be configured with boolean props + +```js +const value = faker.lorem.sentence(200) +;<ConfigurableViewer value={value} bold italic link /> +``` diff --git a/packages/components/xpub-edit/src/components/configurable/EditorOptions.js b/packages/components/xpub-edit/src/components/configurable/EditorOptions.js new file mode 100644 index 0000000000000000000000000000000000000000..0445e48a0b0ffc5fc18c5c3b930250d06d0bf361 --- /dev/null +++ b/packages/components/xpub-edit/src/components/configurable/EditorOptions.js @@ -0,0 +1,49 @@ +import React from 'react' +import makeConfig from './config' + +const FEATURES_WHITELIST = [ + 'bold', + 'italic', + 'underline', + 'superscript', + 'subscript', + 'smallcaps', + 'link', + 'heading', + 'undo', + 'redo', +] + +class ConfigurableEditor extends React.Component { + constructor(props) { + super(props) + this.state = {} + } + + static getDerivedStateFromProps(nextProps, prevState) { + const features = Object.keys(nextProps) + .filter(key => FEATURES_WHITELIST.includes(key)) + .filter(key => nextProps[key]) + + if (prevState.options) { + // updating options on an existing editor is deliberately disabled + // as it causes menu buttons to forget their selected state + return null + } + + return { + options: makeConfig(features), + } + } + + render() { + // only pass through unrecognised props + const remainingProps = Object.keys(this.props) + .filter(key => !FEATURES_WHITELIST.includes(key) && key !== 'children') + .reduce((props, key) => ({ ...props, [key]: this.props[key] }), {}) + + return this.props.children(this.state.options, remainingProps) + } +} + +export default ConfigurableEditor diff --git a/packages/components/xpub-edit/src/components/configurable/EditorOptions.test.js b/packages/components/xpub-edit/src/components/configurable/EditorOptions.test.js new file mode 100644 index 0000000000000000000000000000000000000000..3a2007821c6def0e99957ffe503f66e84b7b63a8 --- /dev/null +++ b/packages/components/xpub-edit/src/components/configurable/EditorOptions.test.js @@ -0,0 +1,33 @@ +import React from 'react' +import { shallow } from 'enzyme' +import EditorOptions from './EditorOptions' + +function makeWrapper(props) { + return shallow(<EditorOptions {...props} />) +} + +describe('EditorOptions', () => { + it('passes menu to child editor', () => { + const children = jest.fn() + makeWrapper({ bold: true, italic: true, underline: false, children }) + + expect(children).toHaveBeenCalled() + expect(children.mock.calls[0][0].menu).toMatchObject([ + { title: 'Toggle bold' }, + { title: 'Toggle italic' }, + ]) + }) + + it('ignores change of props', () => { + const children = jest.fn() + const wrapper = makeWrapper({ bold: true, children }) + wrapper.setProps({ bold: false }) + expect(children.mock.calls[0][0]).toBe(children.mock.calls[1][0]) + }) + + it('passes down arbitrary props', () => { + const children = jest.fn() + makeWrapper({ bold: true, foo: 'bar', children }) + expect(children.mock.calls[0][1]).toEqual({ foo: 'bar' }) + }) +}) diff --git a/packages/components/xpub-edit/src/components/configurable/config/config.test.js b/packages/components/xpub-edit/src/components/configurable/config/config.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1705bb71456530b669dbd899912d3758a6472566 --- /dev/null +++ b/packages/components/xpub-edit/src/components/configurable/config/config.test.js @@ -0,0 +1,34 @@ +import makeConfig from '.' + +describe('makeConfig', () => { + it('turns an array of features into a config', () => { + const config = makeConfig([]) + + expect(config.schema).toBeTruthy() + expect(config.menu).toBeTruthy() + expect(config.plugins).toBeTruthy() + }) + + it('adds marks', () => { + const features = [ + 'bold', + 'italic', + 'underline', + 'link', + 'smallcaps', + 'not real', + 'superscript', + 'subscript', + ] + const config = makeConfig(features) + const realFeatures = features.filter(f => f !== 'not real') + + expect(Object.keys(config.schema.marks)).toEqual(realFeatures) + }) + + it('adds nodes', () => { + const config = makeConfig(['bold', 'heading']) + + expect(Object.keys(config.schema.nodes)).toContain('heading') + }) +}) diff --git a/packages/components/xpub-edit/src/components/configurable/config/index.js b/packages/components/xpub-edit/src/components/configurable/config/index.js index 25297a72f46c5e6bb5b88d6b971ab8d9894d4122..4310350aa176eff2768671176cb4ee3515cb971d 100644 --- a/packages/components/xpub-edit/src/components/configurable/config/index.js +++ b/packages/components/xpub-edit/src/components/configurable/config/index.js @@ -8,17 +8,15 @@ import nodes from './nodes' import marks from './marks' export default features => { - const featureNames = Object.keys(features) - const schema = new Schema({ - marks: pick(marks, featureNames), + marks: pick(marks, features), nodes: { ...pick(nodes, ['doc', 'paragraph', 'text']), - ...pick(nodes, featureNames), + ...pick(nodes, features), }, }) - const enabledMenuItems = pick(menuItems, featureNames) + const enabledMenuItems = pick(menuItems, features) const menu = map(enabledMenuItems, itemCreator => itemCreator(schema)) const plugins = makePlugins(schema, features) diff --git a/packages/components/xpub-edit/src/components/configurable/config/keys.js b/packages/components/xpub-edit/src/components/configurable/config/keys.js index 87f388e3b15ec1c05bf8203b3b33b02c0cba7a05..b00ac82a3ad4530686e3a751036430a6d076e7ea 100644 --- a/packages/components/xpub-edit/src/components/configurable/config/keys.js +++ b/packages/components/xpub-edit/src/components/configurable/config/keys.js @@ -23,10 +23,15 @@ const makeKeymap = (schema, features) => { 'Shift-Ctrl-0': setBlockType(schema.nodes.paragraph), } - if (features.bold) keys['Mod-b'] = toggleMark(schema.marks.bold) - if (features.italic) keys['Mod-i'] = toggleMark(schema.marks.italic) - if (features.heading) + if (features.includes('bold')) { + keys['Mod-b'] = toggleMark(schema.marks.bold) + } + if (features.includes('italic')) { + keys['Mod-i'] = toggleMark(schema.marks.italic) + } + if (features.includes('heading')) { keys['Shift-Ctrl-1'] = setBlockType(schema.nodes.heading, { level: 1 }) + } Object.keys(baseKeymap).forEach(key => { if (keys[key]) { diff --git a/yarn.lock b/yarn.lock index 85a7d506ce1642fcf07b95f8dd69d6251db3e7a0..4da8f5d864b0741094d7573f23f03adf7626abcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -222,6 +222,15 @@ mkdirp "^0.5.1" rimraf "^2.5.2" +"@pubsweet/default-theme@^0.2.0": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@pubsweet/default-theme/-/default-theme-0.2.2.tgz#23a229e74a7bdc63b951efe0f0bbd830fd5faa98" + dependencies: + styled-components "^3.2.5" + typeface-noto-sans "^0.0.54" + typeface-noto-serif "^0.0.54" + typeface-ubuntu-mono "^0.0.54" + "@pubsweet/starter@git+https://gitlab.coko.foundation/pubsweet/pubsweet-starter.git": version "1.0.0-alpha.1" resolved "git+https://gitlab.coko.foundation/pubsweet/pubsweet-starter.git#7ff5635e266415ecb4969658175af8ae9940d791" @@ -251,6 +260,38 @@ pubsweet-theme-plugin "^0.0.3" react-router-redux next +"@pubsweet/theme@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@pubsweet/theme/-/theme-0.1.3.tgz#2c508abc284a33ce14f1fa0a08486477def259a4" + dependencies: + cokourier-prime-sans "git+https://gitlab.coko.foundation/julientaq/cokourier-sans-prime.git" + typeface-fira-mono "^0.0.43" + typeface-fira-sans "^0.0.43" + typeface-fira-sans-condensed "^0.0.43" + typeface-vollkorn "^0.0.43" + +"@pubsweet/ui@^3.0.0": + version "3.3.4" + resolved "https://registry.yarnpkg.com/@pubsweet/ui/-/ui-3.3.4.tgz#f4dda1e900268a7c460f01aec40da187518b6363" + dependencies: + babel-jest "^21.2.0" + classnames "^2.2.5" + enzyme "^3.2.0" + enzyme-adapter-react-16 "^1.1.1" + invariant "^2.2.3" + lodash "^4.17.4" + prop-types "^15.5.10" + react "^16.2.0" + react-dom "^16.2.0" + react-feather "^1.0.8" + react-redux "^5.0.2" + react-router-dom "^4.2.2" + react-tag-autocomplete "^5.5.0" + recompose "^0.26.0" + redux "^3.6.0" + redux-form "^7.0.3" + styled-components "^3.2.5" + "@types/async@2.0.47": version "2.0.47" resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.47.tgz#f49ba1dd1f189486beb6e1d070a850f6ab4bd521" @@ -5538,7 +5579,7 @@ i@0.3.x: version "0.3.6" resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d" -iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13: +iconv-lite@0.4.19, iconv-lite@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -5548,6 +5589,12 @@ iconv-lite@^0.4.4: dependencies: safer-buffer "^2.1.0" +iconv-lite@~0.4.13: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -9481,9 +9528,9 @@ react-docgen@^3.0.0-beta9: node-dir "^0.1.10" recast "^0.12.6" -react-dom@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044" +react-dom@^16.2.0, react-dom@^16.3.2: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.2.tgz#cb90f107e09536d683d84ed5d4888e9640e0e4df" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -9527,6 +9574,10 @@ react-input-autosize@^2.1.2: dependencies: prop-types "^15.5.8" +react-is@^16.3.2: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" + react-moment@^0.6.1: version "0.6.9" resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-0.6.9.tgz#f13a0ccedaf65b5fb8b1467d9f91c5346d74c3e2" @@ -9741,12 +9792,13 @@ react-tag-autocomplete@^5.5.0: resolved "https://registry.yarnpkg.com/react-tag-autocomplete/-/react-tag-autocomplete-5.5.0.tgz#49841388b88323f6bccb0c10039bd0252875b49f" react-test-renderer@^16.0.0-0, react-test-renderer@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.2.0.tgz#bddf259a6b8fcd8555f012afc8eacc238872a211" + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.3.2.tgz#3d1ed74fda8db42521fdf03328e933312214749a" dependencies: fbjs "^0.8.16" object-assign "^4.1.1" prop-types "^15.6.0" + react-is "^16.3.2" react-transition-group@^1.1.2: version "1.2.1" @@ -9769,9 +9821,9 @@ react-transition-group@^2.0.0, react-transition-group@^2.2.0: prop-types "^15.5.8" warning "^3.0.0" -react@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" +react@^16.2.0, react@^16.3.2: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react/-/react-16.3.2.tgz#fdc8420398533a1e58872f59091b272ce2f91ea9" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -10499,7 +10551,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -11783,8 +11835,8 @@ typeface-vollkorn@^0.0.54: resolved "https://registry.yarnpkg.com/typeface-vollkorn/-/typeface-vollkorn-0.0.54.tgz#1288bcd7d81c3dd7cd419e4448580d2a0b0640b2" ua-parser-js@^0.7.9: - version "0.7.17" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" + version "0.7.18" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" uglify-es@^3.3.4: version "3.3.9" @@ -12347,8 +12399,8 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: iconv-lite "0.4.19" whatwg-fetch@>=0.10.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + version "2.0.4" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" whatwg-url@^6.4.0: version "6.4.0"