diff --git a/.babelrc b/.babelrc index 27f321a5..3c04bcb2 100644 --- a/.babelrc +++ b/.babelrc @@ -1,13 +1,4 @@ { - "plugins": [ - [ - "transform-react-remove-prop-types", - { - "mode": "unsafe-wrap", - "ignoreFilenames": ["node_modules"] - } - ] - ], "presets": [ [ "@babel/preset-env", @@ -15,11 +6,16 @@ "modules": false } ], - "@babel/preset-react" + "@babel/preset-react", + "@babel/preset-flow" ], "env": { "test": { - "presets": ["@babel/preset-env", "@babel/preset-react"] + "presets": [ + "@babel/preset-env", + "@babel/preset-react", + "@babel/preset-flow" + ] } } } diff --git a/.eslintrc.js b/.eslintrc.js index 5edd256e..7205a1f9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,8 +1,23 @@ module.exports = { + parser: "babel-eslint", extends: [ "plugin:@thibaudcolas/eslint-plugin-cookbook/recommended", "plugin:compat/recommended", + "plugin:flowtype/recommended", ], + plugins: ["flowtype"], + rules: { + "flowtype/space-after-type-colon": [0], + "flowtype/generic-spacing": [0], + "@thibaudcolas/cookbook/react/require-default-props": [ + "error", + { forbidDefaultForRequired: false }, + ], + "@thibaudcolas/cookbook/react/default-props-match-prop-types": [ + "error", + { allowRequiredDefaults: true }, + ], + }, settings: { polyfills: ["promises"], }, diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 00000000..a8e4b05f --- /dev/null +++ b/.flowconfig @@ -0,0 +1,20 @@ +[ignore] +# .*/node_modules/draft-js/lib/DraftEditorContents.react.js.flow +.*/node_modules/draft-js/lib/ContentBlockNode.js.flow +.*/node_modules/draft-js/lib/ContentBlock.js.flow +.*/node_modules/draft-js/lib/DraftEditorLeaf.react.js.flow +.*/node_modules/draft-js/lib/DraftEditorDragHandler.js.flow +.*/node_modules/draft-js/lib/DraftEditor.react.js.flow +# .*/node_modules/config-chain +suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe + +[include] + +[lints] +all=error +sketchy-null-string=off + +[options] +module.name_mapper='.*\(.scss\)' -> 'empty/object' + +[strict] diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 8faa21f6..40bb7a41 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -24,6 +24,8 @@ SNAPSHOT_STAGED=$(grep .snap$ <<< "$STAGED" || true) HTML_STAGED=$(grep .html$ <<< "$STAGED" || true) YAML_STAGED=$(grep -e '.yml$' -e '.yaml$' <<< "$STAGED" || true) YAML_FULLY_STAGED=$(grep -e '.yml$' -e '.yaml$' <<< "$FULLY_STAGED" || true) +HTML_STAGED=$(grep .html$ <<< "$STAGED" || true) +HTML_FULLY_STAGED=$(grep .html$ <<< "$FULLY_STAGED" || true) # Uncomment, and add more variables to the list, for debugging help. # tr ' ' '\n' <<< "STAGED $STAGED PATCH_STAGED $PATCH_STAGED FULLY_STAGED $FULLY_STAGED JS_STAGED $JS_STAGED JS_FULLY_STAGED $JS_FULLY_STAGED SNAPSHOT_STAGED $SNAPSHOT_STAGED" diff --git a/.githooks/pre-commit.5.prettier.sh b/.githooks/pre-commit.5.prettier.sh index b56be1ab..42ce1621 100755 --- a/.githooks/pre-commit.5.prettier.sh +++ b/.githooks/pre-commit.5.prettier.sh @@ -9,7 +9,7 @@ fi if [ -n "$JS_STAGED" ]; then - npx prettier --list-different $JS_STAGED + npx prettier --check $JS_STAGED fi if [ -n "$SCSS_FULLY_STAGED" ]; @@ -20,7 +20,7 @@ fi if [ -n "$SCSS_STAGED" ]; then - npx prettier --list-different $SCSS_STAGED + npx prettier --check $SCSS_STAGED fi if [ -n "$CSS_FULLY_STAGED" ]; @@ -31,7 +31,7 @@ fi if [ -n "$CSS_STAGED" ]; then - npx prettier --list-different $CSS_STAGED + npx prettier --check $CSS_STAGED fi if [ -n "$MD_FULLY_STAGED" ]; @@ -42,7 +42,7 @@ fi if [ -n "$MD_STAGED" ]; then - npx prettier --list-different $MD_STAGED + npx prettier --check $MD_STAGED fi if [ -n "$JSON_FULLY_STAGED" ]; @@ -53,7 +53,7 @@ fi if [ -n "$JSON_STAGED" ]; then - npx prettier --list-different $JSON_STAGED + npx prettier --check $JSON_STAGED fi if [ -n "$YAML_FULLY_STAGED" ]; @@ -64,5 +64,16 @@ fi if [ -n "$YAML_STAGED" ]; then - npx prettier --list-different $YAML_STAGED + npx prettier --check $YAML_STAGED +fi + +if [ -n "$HTML_FULLY_STAGED" ]; +then + npx prettier --write $HTML_FULLY_STAGED + git add $HTML_FULLY_STAGED +fi + +if [ -n "$HTML_STAGED" ]; +then + npx prettier --check $HTML_STAGED fi diff --git a/.githooks/pre-commit.6.lint.sh b/.githooks/pre-commit.6.lint.sh index 0fcf03b1..005dbb9a 100755 --- a/.githooks/pre-commit.6.lint.sh +++ b/.githooks/pre-commit.6.lint.sh @@ -7,5 +7,12 @@ fi if [ -n "$SCSS_STAGED" ]; then - npx stylelint $SCSS_STAGED + npx stylelint $SCSS_STAGED +fi + +FLOW_STAGED=$(grep -e '.flowconfig$' <<< "$STAGED" || true) + +if [ -n "$JS_STAGED" ] || [ -n "$FLOW_STAGED" ]; +then + npx flow fi diff --git a/.githooks/pre-commit.8.test.sh b/.githooks/pre-commit.8.test.sh index eeb61888..bbac8453 100755 --- a/.githooks/pre-commit.8.test.sh +++ b/.githooks/pre-commit.8.test.sh @@ -2,6 +2,5 @@ if [ -n "$JS_STAGED" ] || [ -n "$SNAPSHOT_STAGED" ]; then - npx flow - npm run test:coverage -s + npm run test:coverage -s fi diff --git a/.githooks/pre-commit.9.build.sh b/.githooks/pre-commit.9.build.sh index 505e5772..b0bbf887 100755 --- a/.githooks/pre-commit.9.build.sh +++ b/.githooks/pre-commit.9.build.sh @@ -2,8 +2,8 @@ if [ -n "$JS_STAGED" ] || [ -n "$SCSS_STAGED" ] || [ -n "$HTML_STAGED" ]; then - npm run dist -s - npm run test:integration -s - npm run test:performance -s - npm run report:size -s + npm run dist -s + npm run test:integration -s + npm run test:performance -s + npm run report:size -s fi diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5530aeba..a52821fe 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -47,6 +47,8 @@ nvm use npm run start # Runs linting. npm run lint +# Start a Flow server for type errors. +npx flow # Re-formats all of the files in the project (with Prettier). npm run format # Run tests in a watcher. diff --git a/.prettierignore b/.prettierignore index ffea70fc..193a9380 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,5 +4,5 @@ coverage/ dist/ webpack-stats.json -public/*.html +public/webpack-stats.html public/storybook diff --git a/.travis.yml b/.travis.yml index c14b2378..b68ca7f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,9 +27,7 @@ env: global: # Permissions: public_repo # travis encrypt --org DANGER_GITHUB_API_TOKEN= - - secure: - "LYb6EEDscuvM3WOCv/pdo3UtdWIdlgzYzo1gXz1T+zMQ+dYLg4wthWrQToBE+Ewon6J0+lA1rdAk3vhmYqObRZcCc6Z8sLNGooxwufAYmeMet0IKwmOqUJhKbVcmxUdhlmo8ksFhGm0A3s3MVnmEy1OO7no0scS1CWPI/SX2KuwMTTYCnoundsvUsbNRFROJXtn4Ra1Ibdzg+iEAt+ROejSRV+Mdd0w0ggRd07v0R+fcje0oEMMD6w4w0AFQha3f+DNpVxVX9FYVS2lLJeKfSeTHWg6GJY+tuzPAmHvDBxBN9w3K2uj8iBcnkD7tEU0yYFCw5gE8r0BTn+0P9EpDQUQilR6BZ84gpbpFl7xoxEef9k7hqWUqTX390cVEl6x0n3FFDO7dl1ZrF/Lavd6K1zMp4swGJEa56c+UNCxymNAfofyNzYDJp/okdvbl0ptz2/riMYjB8dE20B2e/xlspvYE4Icp+LyohEQJQ1+3Hdt7c+3p21qFRIfwWnd7Rpff2awCO37G+CHBwcCCp3GlRLRbtkIdFfKu7/ApHGdF8/1B7Gmae0BD8c/mDCk57pdRn6TvbLekkB/YrpsX+IhWDH0+tgl74V4RYPju80h7FIZMYqOhNOuAtw15SRbIFXCKMeSLuNYYMj7at18Gy7YI699n7aV7PFqY87vxGAWvp7w=" + - secure: "LYb6EEDscuvM3WOCv/pdo3UtdWIdlgzYzo1gXz1T+zMQ+dYLg4wthWrQToBE+Ewon6J0+lA1rdAk3vhmYqObRZcCc6Z8sLNGooxwufAYmeMet0IKwmOqUJhKbVcmxUdhlmo8ksFhGm0A3s3MVnmEy1OO7no0scS1CWPI/SX2KuwMTTYCnoundsvUsbNRFROJXtn4Ra1Ibdzg+iEAt+ROejSRV+Mdd0w0ggRd07v0R+fcje0oEMMD6w4w0AFQha3f+DNpVxVX9FYVS2lLJeKfSeTHWg6GJY+tuzPAmHvDBxBN9w3K2uj8iBcnkD7tEU0yYFCw5gE8r0BTn+0P9EpDQUQilR6BZ84gpbpFl7xoxEef9k7hqWUqTX390cVEl6x0n3FFDO7dl1ZrF/Lavd6K1zMp4swGJEa56c+UNCxymNAfofyNzYDJp/okdvbl0ptz2/riMYjB8dE20B2e/xlspvYE4Icp+LyohEQJQ1+3Hdt7c+3p21qFRIfwWnd7Rpff2awCO37G+CHBwcCCp3GlRLRbtkIdFfKu7/ApHGdF8/1B7Gmae0BD8c/mDCk57pdRn6TvbLekkB/YrpsX+IhWDH0+tgl74V4RYPju80h7FIZMYqOhNOuAtw15SRbIFXCKMeSLuNYYMj7at18Gy7YI699n7aV7PFqY87vxGAWvp7w=" # Permissions: public_repo # travis encrypt --org PAGES_GITHUB_API_TOKEN= - - secure: - "k6h4/xH4iF2dn/CCdLk1xkW1T+ZmtdylMZE4WvaAZ9KwLzz7+5V/ra26df6DFZrqk7wRMHWxWwJa5hRMJfA1wkC5x1/HuDY1KmuEhe75q8edBl1+t+22bI+ESgsRFEh+8KARApRTrJZ06wmZAhreele7rfpV/gVAUyrHz0bkE28oGWVc2PBie7Ygcmz3gHHvYeqGDBWTmoDwADQNgycTgQkUuodM6A2vccOafOhRaguBxizZiIhmJSv0bsViw3QbmpDMC3wenuSjxHs4rKK+N1wv98n7H+6djx8NG//qJdodwUJtD2GXUbTBwPZ+LLMDWJQV2rCpah5b9le75XXm1H1AwDqH32tj8UJENRcNYtRF2rYQOpBDVaYOW6/cMOalY3kl980ACCWpqFPgKNzEBC7z6KXp1BxO1ucciUh3KZ3SY9dpXll+KUm9VRUdkplRuSqc8wDBUmlFn1loMmn17kd4BVLAnB6cd3PpNHbEYYPCH52BRruYHb39gNmJsFvBo9VW03NU/quBxxlC+iqZ7pRgALSGHQgN9UwGpmxGVal5WKXJtBWMijopMD5qktHCHX1mERQBYlVRRTPxz9RZNB/zHBpfptAgKySeEgHWSf4TvaMmC552hGZ0doJ/sqkB8U98ZQ/dVXNTnrXfGEQwynbflVetznXnpMP0VX1hyHo=" + - secure: "k6h4/xH4iF2dn/CCdLk1xkW1T+ZmtdylMZE4WvaAZ9KwLzz7+5V/ra26df6DFZrqk7wRMHWxWwJa5hRMJfA1wkC5x1/HuDY1KmuEhe75q8edBl1+t+22bI+ESgsRFEh+8KARApRTrJZ06wmZAhreele7rfpV/gVAUyrHz0bkE28oGWVc2PBie7Ygcmz3gHHvYeqGDBWTmoDwADQNgycTgQkUuodM6A2vccOafOhRaguBxizZiIhmJSv0bsViw3QbmpDMC3wenuSjxHs4rKK+N1wv98n7H+6djx8NG//qJdodwUJtD2GXUbTBwPZ+LLMDWJQV2rCpah5b9le75XXm1H1AwDqH32tj8UJENRcNYtRF2rYQOpBDVaYOW6/cMOalY3kl980ACCWpqFPgKNzEBC7z6KXp1BxO1ucciUh3KZ3SY9dpXll+KUm9VRUdkplRuSqc8wDBUmlFn1loMmn17kd4BVLAnB6cd3PpNHbEYYPCH52BRruYHb39gNmJsFvBo9VW03NU/quBxxlC+iqZ7pRgALSGHQgN9UwGpmxGVal5WKXJtBWMijopMD5qktHCHX1mERQBYlVRRTPxz9RZNB/zHBpfptAgKySeEgHWSf4TvaMmC552hGZ0doJ/sqkB8U98ZQ/dVXNTnrXfGEQwynbflVetznXnpMP0VX1hyHo=" diff --git a/CHANGELOG.md b/CHANGELOG.md index 82774341..92a10039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,13 @@ ### Changed -- Update [`draftjs-filters`](https://github.com/thibaudcolas/draftjs-filters) dependency to v2.2.1 ([#179](https://github.com/springload/draftail/issues/179)). +- Update [`draftjs-filters`](https://github.com/thibaudcolas/draftjs-filters) dependency ([#179](https://github.com/springload/draftail/issues/179)). +- Update [`draftjs-conductor`](https://github.com/thibaudcolas/draftjs-conductor) dependency. + +### Removed + +- Remove all [`PropTypes`](https://www.npmjs.com/package/prop-types). The project is now typed with [Flow](https://flow.org/) ([#127](https://github.com/springload/draftail/issues/127), [#178](https://github.com/springload/draftail/pull/178)). +- Remove peerDependency on `prop-types` ([#127](https://github.com/springload/draftail/issues/127), [#178](https://github.com/springload/draftail/pull/178)). ## [[v1.0.0]](https://github.com/springload/draftail/releases/tag/v1.0.0) diff --git a/README.md b/README.md index 84f4e442..0f0e4478 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Here are important features worth highlighting: - Common text styles: Bold, italic, and many more. - API to build custom controls for links, images, and more. -> This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html), and measures performance and [code coverage](https://coveralls.io/github/springload/draftail). We also try to follow accessibility best practices (tested with [aXe](https://www.axe-core.org/)) – please [get in touch](https://github.com/springload/draftail/issues/149#issuecomment-389476151) if you can help us do better in this area. +> This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html), and measures performance and [code coverage](https://coveralls.io/github/springload/draftail). It uses [Flow](https://flow.org/) types. We also try to follow accessibility best practices (tested with [aXe](https://www.axe-core.org/)) – please [get in touch](https://github.com/springload/draftail/issues/149#issuecomment-389476151) if you can help us do better in this area. ## Documentation diff --git a/docs/README.md b/docs/README.md index dc3e387a..1e74ea7b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,6 +28,7 @@ Here are specific parts of the code that **should always be reviewed before upgr - https://github.com/springload/draftail/blob/df903f86c882bd5101eb05e152e8b8a8b9a4915e/lib/api/behavior.js#L23:L26 - https://github.com/springload/draftail/commit/88ae9adcda1929c92f065655a03c1b33fcfe6c2d - https://github.com/springload/draftail/commit/e05df07f8ed6c5df65c79824bbb1dcd6e8800bdd +- Type errors silenced with `$FlowFixMe` ## Troubleshooting diff --git a/examples/blocks/EmbedBlock.js b/examples/blocks/EmbedBlock.js index 8b620aba..6921b9df 100644 --- a/examples/blocks/EmbedBlock.js +++ b/examples/blocks/EmbedBlock.js @@ -1,12 +1,19 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; +import type { ContentBlock } from "draft-js"; import MediaBlock from "./MediaBlock"; +import type { BlockProps } from "./MediaBlock"; + +type Props = {| + block: ContentBlock, + blockProps: BlockProps, +|}; /** * Editor block to display media and edit content. */ -const EmbedBlock = (props) => { +const EmbedBlock = (props: Props) => { const { blockProps } = props; const { entity, onEditEntity, onRemoveEntity } = blockProps; const { url, title, thumbnail } = entity.getData(); @@ -38,10 +45,4 @@ const EmbedBlock = (props) => { ); }; -EmbedBlock.propTypes = { - blockProps: PropTypes.shape({ - entity: PropTypes.object, - }).isRequired, -}; - export default EmbedBlock; diff --git a/examples/blocks/ImageBlock.js b/examples/blocks/ImageBlock.js index 6018e774..2b346038 100644 --- a/examples/blocks/ImageBlock.js +++ b/examples/blocks/ImageBlock.js @@ -1,28 +1,38 @@ -import PropTypes from "prop-types"; +// @flow import React, { Component } from "react"; +import type { ContentBlock } from "draft-js"; import MediaBlock from "./MediaBlock"; +import type { BlockProps } from "./MediaBlock"; import { DraftUtils } from "../../lib/index"; +type Props = {| + block: ContentBlock, + blockProps: BlockProps, +|}; + /** * Editor block to preview and edit images. */ -class ImageBlock extends Component { - constructor(props) { +class ImageBlock extends Component { + constructor(props: Props) { super(props); this.changeAlt = this.changeAlt.bind(this); } - changeAlt(e) { + /* :: changeAlt: (e: Event) => void; */ + changeAlt(e: Event) { const { block, blockProps } = this.props; const { editorState, onChange } = blockProps; - const data = { - alt: e.currentTarget.value, - }; + if (e.currentTarget instanceof HTMLInputElement) { + const data = { + alt: e.currentTarget.value, + }; - onChange(DraftUtils.updateBlockEntity(editorState, block, data)); + onChange(DraftUtils.updateBlockEntity(editorState, block, data)); + } } render() { @@ -56,13 +66,4 @@ class ImageBlock extends Component { } } -ImageBlock.propTypes = { - block: PropTypes.object.isRequired, - blockProps: PropTypes.shape({ - editorState: PropTypes.object.isRequired, - entity: PropTypes.object, - onChange: PropTypes.func.isRequired, - }).isRequired, -}; - export default ImageBlock; diff --git a/examples/blocks/ImageBlock.test.js b/examples/blocks/ImageBlock.test.js index c3b380a8..5d53e24b 100644 --- a/examples/blocks/ImageBlock.test.js +++ b/examples/blocks/ImageBlock.test.js @@ -69,11 +69,10 @@ describe("ImageBlock", () => { />, ); - wrapper.find('[type="text"]').simulate("change", { - currentTarget: { - value: "new alt", - }, - }); + const currentTarget = document.createElement("input"); + currentTarget.value = "new alt"; + + wrapper.find('[type="text"]').simulate("change", { currentTarget }); expect(onChange).toHaveBeenCalled(); expect(DraftUtils.updateBlockEntity).toHaveBeenCalledWith( diff --git a/examples/blocks/MediaBlock.js b/examples/blocks/MediaBlock.js index 0c532306..8de415d0 100644 --- a/examples/blocks/MediaBlock.js +++ b/examples/blocks/MediaBlock.js @@ -1,9 +1,13 @@ -import PropTypes from "prop-types"; +// @flow import React, { Component } from "react"; +import type { Node } from "react"; +import { EditorState } from "draft-js"; +import type { EntityInstance } from "draft-js"; import { Icon } from "../../lib"; import Tooltip from "../components/Tooltip"; +import type { Rect } from "../components/Tooltip"; import Portal from "../components/Portal"; // Constraints the maximum size of the tooltip. @@ -11,15 +15,41 @@ const OPTIONS_MAX_WIDTH = 300; const OPTIONS_SPACING = 70; const TOOLTIP_MAX_WIDTH = OPTIONS_MAX_WIDTH + OPTIONS_SPACING; +export type BlockProps = {| + entity: EntityInstance, + entityType: { + description: string, + icon: string | string[] | Node, + }, + editorState: EditorState, + onChange: (EditorState) => void, + onEditEntity: () => void, + onRemoveEntity: () => void, +|}; + +type Props = { + blockProps: BlockProps, + src: string, + label: string, + children: Node, +}; + +type State = {| + tooltip: ?{| + target: Rect, + containerWidth: number, + |}, +|}; + /** * Editor block to preview and edit images. */ -class MediaBlock extends Component { - constructor(props) { +class MediaBlock extends Component { + constructor(props: Props) { super(props); this.state = { - showTooltipAt: null, + tooltip: null, }; this.openTooltip = this.openTooltip.bind(this); @@ -27,24 +57,40 @@ class MediaBlock extends Component { this.renderTooltip = this.renderTooltip.bind(this); } - openTooltip(e) { + /* :: openTooltip: (e: Event) => void; */ + openTooltip(e: Event) { const trigger = e.target; - this.setState({ - showTooltipAt: Object.assign(trigger.getBoundingClientRect(), { - containerWidth: trigger.parentNode.offsetWidth, - }), - }); + if ( + trigger instanceof Element && + trigger.parentNode instanceof HTMLElement + ) { + const containerWidth = trigger.parentNode.offsetWidth; + + this.setState({ + tooltip: { + target: trigger.getBoundingClientRect(), + containerWidth, + }, + }); + } } + /* :: closeTooltip: () => void; */ closeTooltip() { - this.setState({ showTooltipAt: null }); + this.setState({ tooltip: null }); } + /* :: renderTooltip: () => ?Node; */ renderTooltip() { const { children } = this.props; - const { showTooltipAt } = this.state; - const maxWidth = showTooltipAt.containerWidth - showTooltipAt.width; + const { tooltip } = this.state; + + if (!tooltip) { + return null; + } + + const maxWidth = tooltip.containerWidth - tooltip.target.width; const direction = maxWidth >= TOOLTIP_MAX_WIDTH ? "left" : "top-left"; return ( @@ -54,7 +100,7 @@ class MediaBlock extends Component { closeOnType closeOnResize > - +
{children}
@@ -63,7 +109,6 @@ class MediaBlock extends Component { render() { const { blockProps, src, label } = this.props; - const { showTooltipAt } = this.state; const { entityType } = blockProps; return ( @@ -80,20 +125,10 @@ class MediaBlock extends Component { - {showTooltipAt && this.renderTooltip()} + {this.renderTooltip()} ); } } -MediaBlock.propTypes = { - blockProps: PropTypes.shape({ - entity: PropTypes.object, - entityType: PropTypes.object.isRequired, - }).isRequired, - src: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, -}; - export default MediaBlock; diff --git a/examples/blocks/MediaBlock.test.js b/examples/blocks/MediaBlock.test.js index bacf9655..a66171a4 100644 --- a/examples/blocks/MediaBlock.test.js +++ b/examples/blocks/MediaBlock.test.js @@ -94,19 +94,21 @@ describe("MediaBlock", () => { jest.spyOn(target, "getBoundingClientRect"); - expect(wrapper.state("showTooltipAt")).toBe(null); + expect(wrapper.state("tooltip")).toBe(null); wrapper.simulate("mouseup", { target }); - expect(wrapper.state("showTooltipAt")).toMatchObject({ - top: 0, - left: 0, + expect(wrapper.state("tooltip")).toMatchObject({ + target: { + top: 0, + left: 0, + }, }); expect(target.getBoundingClientRect).toHaveBeenCalled(); wrapper.instance().closeTooltip(); - expect(wrapper.state("showTooltipAt")).toBe(null); + expect(wrapper.state("tooltip")).toBe(null); }); }); }); diff --git a/examples/components/BenchmarkResults.js b/examples/components/BenchmarkResults.js index 3e56a417..221fa310 100644 --- a/examples/components/BenchmarkResults.js +++ b/examples/components/BenchmarkResults.js @@ -1,42 +1,38 @@ +// @flow import React from "react"; -import PropTypes from "prop-types"; +import type { BenchResultsType } from "react-component-benchmark"; -const BenchmarkResults = ({ results }) => - results ? ( - - - - - - - - - - - - - - - - - - - - - - - - - -
meanminmedianp70p95p99maxstdDev
{results.mean.toFixed(1)}{results.min.toFixed(1)}{results.median.toFixed(1)}{results.p70.toFixed(1)}{results.p95.toFixed(1)}{results.p99.toFixed(1)}{results.max.toFixed(1)}{results.stdDev.toFixed(1)}
- ) : null; - -BenchmarkResults.propTypes = { - results: PropTypes.object, +type Props = { + results: BenchResultsType, }; -BenchmarkResults.defaultProps = { - results: null, -}; +const BenchmarkResults = ({ results }: Props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + +
meanminmedianp70p95p99maxstdDev
{results.mean.toFixed(1)}{results.min.toFixed(1)}{results.median.toFixed(1)}{results.p70.toFixed(1)}{results.p95.toFixed(1)}{results.p99.toFixed(1)}{results.max.toFixed(1)}{results.stdDev.toFixed(1)}
+); export default BenchmarkResults; diff --git a/examples/components/EditorBenchmark.js b/examples/components/EditorBenchmark.js index 95d6c1fe..abe0ffa1 100644 --- a/examples/components/EditorBenchmark.js +++ b/examples/components/EditorBenchmark.js @@ -1,13 +1,23 @@ -import PropTypes from "prop-types"; +// @flow import React, { Component } from "react"; import Benchmark, { BenchmarkType } from "react-component-benchmark"; +import type { BenchResultsType } from "react-component-benchmark"; import { DraftailEditor } from "../../lib"; import BenchmarkResults from "./BenchmarkResults"; -class EditorBenchmark extends Component { - constructor(props) { +type Props = {| + componentProps: {}, + runOnMount: boolean, +|}; + +type State = {| + results: ?BenchResultsType, +|}; + +class EditorBenchmark extends Component { + constructor(props: Props) { super(props); this.state = { @@ -26,10 +36,14 @@ class EditorBenchmark extends Component { } } - onBenchmarkComplete(results) { + /* :: onBenchmarkComplete: (results: BenchResultsType) => void; */ + onBenchmarkComplete(results: BenchResultsType) { this.setState({ results }); } + benchmark: ?Benchmark; + + /* :: startBenchmark: () => void; */ startBenchmark() { if (this.benchmark) { this.benchmark.start(); @@ -56,19 +70,10 @@ class EditorBenchmark extends Component { timeout={10000} type={BenchmarkType.MOUNT} /> - + {results ? : null} ); } } -EditorBenchmark.propTypes = { - componentProps: PropTypes.object.isRequired, - runOnMount: PropTypes.bool, -}; - -EditorBenchmark.defaultProps = { - runOnMount: false, -}; - export default EditorBenchmark; diff --git a/examples/components/EditorWrapper.js b/examples/components/EditorWrapper.js index c5c4f5d4..277eca59 100644 --- a/examples/components/EditorWrapper.js +++ b/examples/components/EditorWrapper.js @@ -1,5 +1,6 @@ -import PropTypes from "prop-types"; +// @flow import React, { Component } from "react"; +import type { RawDraftContentState } from "draft-js/lib/RawDraftContentState"; import { DraftailEditor } from "../../lib"; @@ -11,8 +12,18 @@ import EditorBenchmark from "./EditorBenchmark"; const DRAFTAIL_VERSION = typeof PKG_VERSION === "undefined" ? "dev" : PKG_VERSION; -class EditorWrapper extends Component { - constructor(props) { +type Props = {| + id: string, + onSave: ?(?RawDraftContentState) => void, +|}; + +type State = {| + content: ?RawDraftContentState, + saveCount: number, +|}; + +class EditorWrapper extends Component { + constructor(props: Props) { super(props); this.state = { @@ -23,7 +34,8 @@ class EditorWrapper extends Component { this.onSave = this.onSave.bind(this); } - onSave(content) { + /* :: onSave: (content: ?RawDraftContentState) => void; */ + onSave(content: ?RawDraftContentState) { const { id, onSave } = this.props; this.setState(({ saveCount }) => ({ content, saveCount: saveCount + 1 })); @@ -36,16 +48,16 @@ class EditorWrapper extends Component { } render() { - const { id } = this.props; + const { id, onSave, ...editorProps } = this.props; const { content, saveCount } = this.state; - const initialContent = - JSON.parse(sessionStorage.getItem(`${id}:content`)) || null; + const storedContent = sessionStorage.getItem(`${id}:content`) || null; + const initialContent = storedContent ? JSON.parse(storedContent) : null; return (
@@ -63,7 +75,6 @@ class EditorWrapper extends Component { @@ -72,13 +83,4 @@ class EditorWrapper extends Component { } } -EditorWrapper.propTypes = { - id: PropTypes.string.isRequired, - onSave: PropTypes.func, -}; - -EditorWrapper.defaultProps = { - onSave: () => {}, -}; - export default EditorWrapper; diff --git a/examples/components/FontIcon.js b/examples/components/FontIcon.js index c14dd21a..3a2f162c 100644 --- a/examples/components/FontIcon.js +++ b/examples/components/FontIcon.js @@ -1,12 +1,10 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; -const FontIcon = ({ icon }) => ( +type Props = {| icon: string |}; + +const FontIcon = ({ icon }: Props) => ( ); -FontIcon.propTypes = { - icon: PropTypes.string.isRequired, -}; - export default FontIcon; diff --git a/examples/components/Highlight.js b/examples/components/Highlight.js index b11277c7..17c47d0d 100644 --- a/examples/components/Highlight.js +++ b/examples/components/Highlight.js @@ -1,16 +1,20 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; const onCopy = (value) => { const hidden = document.createElement("textarea"); hidden.value = value; + // $FlowFixMe document.body.appendChild(hidden); hidden.select(); document.execCommand("copy"); + // $FlowFixMe document.body.removeChild(hidden); }; -const Highlight = ({ value }) => ( +type Props = {| value: string |}; + +const Highlight = ({ value }: Props) => (
     
); -Highlight.propTypes = { - value: PropTypes.string.isRequired, -}; - export default Highlight; diff --git a/examples/components/Modal.js b/examples/components/Modal.js index 0056e12f..85776f10 100644 --- a/examples/components/Modal.js +++ b/examples/components/Modal.js @@ -1,4 +1,6 @@ +// @flow import React from "react"; +import type { Node } from "react"; import ReactModal from "react-modal"; const className = { @@ -13,7 +15,15 @@ const overlayClassName = { beforeClose: "modal__overlay--before-close", }; -const Modal = (props) => ( +type Props = {| + onAfterOpen: () => void | Promise, + onRequestClose: (SyntheticEvent<>) => void, + isOpen: boolean, + contentLabel: string, + children: Node, +|}; + +const Modal = (props: Props) => ( void, +|}; + +class Portal extends Component { + static defaultProps: DefaultProps; + + constructor(props: Props) { super(props); this.onCloseEvent = this.onCloseEvent.bind(this); @@ -14,7 +29,10 @@ class Portal extends Component { if (!this.portal) { this.portal = document.createElement("div"); - document.body.appendChild(this.portal); + + if (document.body) { + document.body.appendChild(this.portal); + } if (onClose) { if (closeOnClick) { @@ -43,36 +61,32 @@ class Portal extends Component { componentWillUnmount() { const { onClose } = this.props; - document.body.removeChild(this.portal); + if (document.body) { + document.body.removeChild(this.portal); + } document.removeEventListener("mouseup", this.onCloseEvent); document.removeEventListener("keyup", this.onCloseEvent); window.removeEventListener("resize", onClose); } - onCloseEvent(e) { + /* :: onCloseEvent: (e: Event) => void; */ + onCloseEvent(e: Event) { const { onClose } = this.props; - if (!this.portal.contains(e.target)) { + if (e.target instanceof Element && !this.portal.contains(e.target)) { onClose(); } } + portal: Element; + render() { return null; } } -Portal.propTypes = { - onClose: PropTypes.func, - children: PropTypes.node, - closeOnClick: PropTypes.bool, - closeOnType: PropTypes.bool, - closeOnResize: PropTypes.bool, -}; - Portal.defaultProps = { - onClose: null, children: null, closeOnClick: false, closeOnType: false, diff --git a/examples/components/PrismDecorator.js b/examples/components/PrismDecorator.js index 24ad7fdc..f1c758cb 100644 --- a/examples/components/PrismDecorator.js +++ b/examples/components/PrismDecorator.js @@ -1,15 +1,27 @@ +// @flow import React from "react"; +import type { Node } from "react"; import Prism from "prismjs"; +import type { ContentBlock } from "draft-js"; import { BLOCK_TYPE } from "../../lib"; +type Options = {| + defaultLanguage: "javascript" | "css", +|}; + /** * Syntax highlighting with Prism as a Draft.js decorator. * This code is an adaptation of https://github.com/SamyPesse/draft-js-prism * to use the CompositeDecorator strategy API. */ class PrismDecorator { - constructor(options) { + /* :: options: Options; */ + /* :: highlighted: {}; */ + /* :: component: (props: { children: Node, offsetKey: string }) => Node; */ + /* :: strategy: (block: ContentBlock, (start: number, end: number) => void) => void; */ + + constructor(options: Options) { this.options = options; this.highlighted = {}; @@ -18,19 +30,22 @@ class PrismDecorator { } // Renders the decorated tokens. - renderToken({ children, offsetKey }) { + renderToken({ children, offsetKey }: { children: Node, offsetKey: string }) { const type = this.getTokenTypeForKey(offsetKey); return {children}; } - getTokenTypeForKey(key) { + getTokenTypeForKey(key: string) { const [blockKey, tokId] = key.split("-"); const token = this.highlighted[blockKey][tokId]; return token ? token.type : ""; } - getDecorations(block, callback) { + getDecorations( + block: ContentBlock, + callback: (start: number, end: number) => void, + ) { // Only process code blocks. if (block.getType() !== BLOCK_TYPE.CODE) { return; diff --git a/examples/components/ReadingTime.js b/examples/components/ReadingTime.js index 13870237..dbb1f2c6 100644 --- a/examples/components/ReadingTime.js +++ b/examples/components/ReadingTime.js @@ -1,16 +1,21 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; import readingTime from "reading-time"; +import { EditorState } from "draft-js"; import { ToolbarButton } from "../../lib"; const CLOCK_ICON = "M658.744 749.256l-210.744-210.746v-282.51h128v229.49l173.256 173.254zM512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 896c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.078 0 384 171.922 384 384s-171.922 384-384 384z"; +type Props = {| + getEditorState: () => EditorState, +|}; + /** * A basic control showing the reading time / content length for the editor’s content. */ -const ReadingTime = ({ getEditorState }) => { +const ReadingTime = ({ getEditorState }: Props) => { const editorState = getEditorState(); const content = editorState.getCurrentContent(); const text = content.getPlainText(); @@ -28,8 +33,4 @@ const ReadingTime = ({ getEditorState }) => { ); }; -ReadingTime.propTypes = { - getEditorState: PropTypes.func.isRequired, -}; - export default ReadingTime; diff --git a/examples/components/SentryBoundary.js b/examples/components/SentryBoundary.js index dcf2a68c..19f4f277 100644 --- a/examples/components/SentryBoundary.js +++ b/examples/components/SentryBoundary.js @@ -1,11 +1,21 @@ -import PropTypes from "prop-types"; +// @flow import React, { Component } from "react"; +import type { Node } from "react"; const { Raven } = window; const isRavenAvailable = !!Raven; -class SentryBoundary extends Component { - constructor(props) { +type Props = {| + children: Node, +|}; + +type State = {| + error: ?Error, + reloads: number, +|}; + +class SentryBoundary extends Component { + constructor(props: Props) { super(props); this.state = { error: null, @@ -15,6 +25,7 @@ class SentryBoundary extends Component { this.onAttemptReload = this.onAttemptReload.bind(this); } + /* :: onAttemptReload: () => void; */ onAttemptReload() { const { reloads } = this.state; @@ -28,7 +39,12 @@ class SentryBoundary extends Component { } } - componentDidCatch(error, errorInfo) { + componentDidCatch( + error: Error, + errorInfo: { + componentStack: string, + }, + ) { this.setState({ error }); if (isRavenAvailable) { @@ -97,8 +113,4 @@ class SentryBoundary extends Component { } } -SentryBoundary.propTypes = { - children: PropTypes.node.isRequired, -}; - export default SentryBoundary; diff --git a/examples/components/StatsGraph.js b/examples/components/StatsGraph.js index 5fd5c806..25a05f99 100644 --- a/examples/components/StatsGraph.js +++ b/examples/components/StatsGraph.js @@ -1,3 +1,4 @@ +// @flow import React, { Component } from "react"; import Stats from "stats-js"; @@ -7,18 +8,14 @@ const MB_PANEL = 2; * Integration of https://github.com/Kevnz/stats.js. * Adapted from https://github.com/vigneshshanmugam/react-memorystats. */ -export default class StatsGraph extends Component { - constructor(props) { - super(props); - +export default class StatsGraph extends Component<{||}> { + componentDidMount() { const stats = new Stats(); stats.showPanel(MB_PANEL); stats.begin(); this.stats = stats; - } - componentDidMount() { if (this.elt) { this.elt.appendChild(this.stats.domElement); } @@ -33,6 +30,10 @@ export default class StatsGraph extends Component { } } + stats: Stats; + + elt: ?Element; + render() { return (
{ +export type Rect = { + top: number, + left: number, + width: number, + height: number, +}; + +type Direction = "top" | "left" | "top-left"; + +const getTooltipStyles = ( + target: Rect, + direction: Direction, +): {| top: number, left: number |} => { const top = window.pageYOffset + target.top; const left = window.pageXOffset + target.left; switch (direction) { @@ -28,10 +41,16 @@ const getTooltipStyles = (target, direction) => { } }; +type Props = {| + target: Rect, + children: Node, + direction: Direction, +|}; + /** * A tooltip, with arbitrary content. */ -const Tooltip = ({ target, children, direction }) => ( +const Tooltip = ({ target, children, direction }: Props) => (
(
); -Tooltip.propTypes = { - target: PropTypes.shape({ - top: PropTypes.number.isRequired, - left: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - direction: PropTypes.oneOf([TOP, LEFT, TOP_LEFT]).isRequired, - children: PropTypes.node.isRequired, -}; - export default Tooltip; diff --git a/examples/constants/allContentState.js b/examples/constants/allContentState.js index 4b2f6b49..8d92117c 100644 --- a/examples/constants/allContentState.js +++ b/examples/constants/allContentState.js @@ -1,3 +1,4 @@ +// @flow export default { entityMap: { "0": { diff --git a/examples/constants/customContentState.js b/examples/constants/customContentState.js index ca6e1629..dd7be14e 100644 --- a/examples/constants/customContentState.js +++ b/examples/constants/customContentState.js @@ -1,3 +1,4 @@ +// @flow export default { entityMap: { "0": { diff --git a/examples/constants/indexContentState.js b/examples/constants/indexContentState.js index 534fce5a..9965781b 100644 --- a/examples/constants/indexContentState.js +++ b/examples/constants/indexContentState.js @@ -1,3 +1,4 @@ +// @flow export default { entityMap: { "0": { diff --git a/examples/entities/Document.js b/examples/entities/Document.js index cb5b363c..70d07c13 100644 --- a/examples/entities/Document.js +++ b/examples/entities/Document.js @@ -1,22 +1,43 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; +import type { Node } from "react"; +import { ContentState } from "draft-js"; import FontIcon from "../components/FontIcon"; import TooltipEntity from "./TooltipEntity"; export const DOCUMENT_ICON = ; -const Document = (props) => { - const { entityKey, contentState } = props; +type Props = {| + entityKey: string, + contentState: ContentState, + children: Node, + onEdit: (string) => void, + onRemove: (string) => void, +|}; + +const Document = ({ + entityKey, + contentState, + children, + onEdit, + onRemove, +}: Props) => { const { url, id } = contentState.getEntity(entityKey).getData(); // Supports documents defined based on a URL, and id. const label = url ? url.replace(/(^\w+:|^)\/\//, "").split("/")[0] : id; - return ; -}; - -Document.propTypes = { - entityKey: PropTypes.string.isRequired, - contentState: PropTypes.object.isRequired, + return ( + + {children} + + ); }; export default Document; diff --git a/examples/entities/Link.js b/examples/entities/Link.js index c2dd015e..86b35781 100644 --- a/examples/entities/Link.js +++ b/examples/entities/Link.js @@ -1,21 +1,42 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; +import type { Node } from "react"; +import { ContentState } from "draft-js"; import TooltipEntity from "./TooltipEntity"; -const Link = (props) => { - const { entityKey, contentState } = props; +type Props = {| + entityKey: string, + contentState: ContentState, + children: Node, + onEdit: (string) => void, + onRemove: (string) => void, +|}; + +const Link = ({ + entityKey, + contentState, + children, + onEdit, + onRemove, +}: Props) => { const { url, linkType } = contentState.getEntity(entityKey).getData(); const isEmailLink = linkType === "email" || url.startsWith("mailto:"); const icon = `#icon-${isEmailLink ? "mail" : "link"}`; const label = url.replace(/(^\w+:|^)\/\//, "").split("/")[0]; - return ; -}; - -Link.propTypes = { - entityKey: PropTypes.string.isRequired, - contentState: PropTypes.object.isRequired, + return ( + + {children} + + ); }; export default Link; diff --git a/examples/entities/TooltipEntity.js b/examples/entities/TooltipEntity.js index 4cf2f2fc..c6d6cc38 100644 --- a/examples/entities/TooltipEntity.js +++ b/examples/entities/TooltipEntity.js @@ -1,13 +1,35 @@ -import PropTypes from "prop-types"; +// @flow import React, { Component } from "react"; +import type { Node } from "react"; +import { ContentState } from "draft-js"; import { Icon } from "../../lib"; import Tooltip from "../components/Tooltip"; +import type { Rect } from "../components/Tooltip"; import Portal from "../components/Portal"; -class TooltipEntity extends Component { - constructor(props) { +type Props = { + // Key of the entity being decorated. + entityKey: string, + // Full contentState, read-only. + contentState: ContentState, + // The decorated nodes / entity text. + children: Node, + // Call with the entityKey to trigger the entity source. + onEdit: (string) => void, + // Call with the entityKey to remove the entity. + onRemove: (string) => void, + icon: string | Node, + label: string, +}; + +type State = { + showTooltipAt: ?Rect, +}; + +class TooltipEntity extends Component { + constructor(props: Props) { super(props); this.state = { @@ -18,11 +40,16 @@ class TooltipEntity extends Component { this.closeTooltip = this.closeTooltip.bind(this); } - openTooltip(e) { + /* :: openTooltip: (e: Event) => void; */ + openTooltip(e: Event) { const trigger = e.target; - this.setState({ showTooltipAt: trigger.getBoundingClientRect() }); + + if (trigger instanceof Element) { + this.setState({ showTooltipAt: trigger.getBoundingClientRect() }); + } } + /* :: closeTooltip: () => void; */ closeTooltip() { this.setState({ showTooltipAt: null }); } @@ -87,22 +114,4 @@ class TooltipEntity extends Component { } } -TooltipEntity.propTypes = { - // Key of the entity being decorated. - entityKey: PropTypes.string.isRequired, - // Full contentState, read-only. - contentState: PropTypes.object.isRequired, - // The decorated nodes / entity text. - children: PropTypes.node.isRequired, - // Call with the entityKey to trigger the entity source. - onEdit: PropTypes.func.isRequired, - // Call with the entityKey to remove the entity. - onRemove: PropTypes.func.isRequired, - icon: PropTypes.oneOfType([ - PropTypes.string.isRequired, - PropTypes.object.isRequired, - ]).isRequired, - label: PropTypes.string.isRequired, -}; - export default TooltipEntity; diff --git a/examples/sources/DocumentSource.js b/examples/sources/DocumentSource.js index b4f25708..91391fc4 100644 --- a/examples/sources/DocumentSource.js +++ b/examples/sources/DocumentSource.js @@ -1,12 +1,26 @@ +// @flow import React, { Component } from "react"; -import PropTypes from "prop-types"; - -import { RichUtils } from "draft-js"; +import { RichUtils, EditorState } from "draft-js"; +import type { EntityInstance } from "draft-js"; import Modal from "../components/Modal"; -class DocumentSource extends Component { - constructor(props) { +type Props = {| + editorState: EditorState, + onComplete: (EditorState) => void, + onClose: () => void, + entityType: { + type: string, + }, + entity: ?EntityInstance, +|}; + +type State = {| + url: string, +|}; + +class DocumentSource extends Component { + constructor(props: Props) { super(props); const { entity } = this.props; @@ -27,7 +41,8 @@ class DocumentSource extends Component { this.onChangeURL = this.onChangeURL.bind(this); } - onConfirm(e) { + /* :: onConfirm: (e: Event) => void; */ + onConfirm(e: Event) { const { editorState, entityType, onComplete } = this.props; const { url } = this.state; @@ -39,6 +54,8 @@ class DocumentSource extends Component { url: url.replace(/\s/g, ""), }; const contentStateWithEntity = contentState.createEntity( + // Fixed in https://github.com/facebook/draft-js/commit/6ba124cf663b78c41afd6c361a67bd29724fa617, to be released. + // $FlowFixMe entityType.type, "MUTABLE", data, @@ -53,25 +70,34 @@ class DocumentSource extends Component { onComplete(nextState); } - onRequestClose(e) { + /* :: onRequestClose: (e: SyntheticEvent<>) => void; */ + onRequestClose(e: SyntheticEvent<>) { const { onClose } = this.props; e.preventDefault(); onClose(); } + /* :: onAfterOpen: () => void; */ onAfterOpen() { - if (this.inputRef) { - this.inputRef.focus(); - this.inputRef.select(); + const input = this.inputRef; + + if (input) { + input.focus(); + input.select(); } } - onChangeURL(e) { - const url = e.target.value; - this.setState({ url }); + /* :: onChangeURL: (e: Event) => void; */ + onChangeURL(e: Event) { + if (e.target instanceof HTMLInputElement) { + const url = e.target.value; + this.setState({ url }); + } } + inputRef: ?HTMLInputElement; + render() { const { url } = this.state; return ( @@ -102,16 +128,4 @@ class DocumentSource extends Component { } } -DocumentSource.propTypes = { - editorState: PropTypes.object.isRequired, - onComplete: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - entityType: PropTypes.object.isRequired, - entity: PropTypes.object, -}; - -DocumentSource.defaultProps = { - entity: null, -}; - export default DocumentSource; diff --git a/examples/sources/EmbedSource.js b/examples/sources/EmbedSource.js index 6572e639..fa165496 100644 --- a/examples/sources/EmbedSource.js +++ b/examples/sources/EmbedSource.js @@ -1,6 +1,7 @@ -import PropTypes from "prop-types"; -import React from "react"; +// @flow +import React, { Component } from "react"; import { AtomicBlockUtils, EditorState } from "draft-js"; +import type { EntityInstance } from "draft-js"; import Modal from "../components/Modal"; @@ -20,8 +21,23 @@ const getJSON = (endpoint, data, successCallback) => { request.send(data); }; -class EmbedSource extends React.Component { - constructor(props) { +type Props = {| + editorState: EditorState, + onComplete: (EditorState) => void, + onClose: () => void, + entityType: { + type: string, + }, + entity: ?EntityInstance, + entityKey: ?string, +|}; + +type State = {| + url: string, +|}; + +class EmbedSource extends Component { + constructor(props: Props) { super(props); const { entity } = this.props; @@ -42,7 +58,8 @@ class EmbedSource extends React.Component { this.onChangeSource = this.onChangeSource.bind(this); } - onConfirm(e) { + /* :: onConfirm: (e: Event) => void; */ + onConfirm(e: Event) { const { editorState, entity, @@ -60,7 +77,7 @@ class EmbedSource extends React.Component { `${EMBEDLY_ENDPOINT}&url=${encodeURIComponent(url)}`, null, (embed) => { - if (entity) { + if (entity && entityKey) { const nextContent = content.mergeEntityData(entityKey, { url: embed.url, title: embed.title, @@ -73,6 +90,8 @@ class EmbedSource extends React.Component { ); } else { const contentWithEntity = content.createEntity( + // Fixed in https://github.com/facebook/draft-js/commit/6ba124cf663b78c41afd6c361a67bd29724fa617, to be released. + // $FlowFixMe entityType.type, "IMMUTABLE", { @@ -94,25 +113,34 @@ class EmbedSource extends React.Component { ); } - onRequestClose(e) { + /* :: onRequestClose: (e: SyntheticEvent<>) => void; */ + onRequestClose(e: SyntheticEvent<>) { const { onClose } = this.props; e.preventDefault(); onClose(); } + /* :: onAfterOpen: () => void; */ onAfterOpen() { - if (this.inputRef) { - this.inputRef.focus(); - this.inputRef.select(); + const input = this.inputRef; + + if (input) { + input.focus(); + input.select(); } } - onChangeSource(e) { - const url = e.target.value; - this.setState({ url }); + /* :: onChangeSource: (e: Event) => void; */ + onChangeSource(e: Event) { + if (e.target instanceof HTMLInputElement) { + const url = e.target.value; + this.setState({ url }); + } } + inputRef: ?HTMLInputElement; + render() { const { url } = this.state; return ( @@ -145,18 +173,4 @@ class EmbedSource extends React.Component { } } -EmbedSource.propTypes = { - editorState: PropTypes.object.isRequired, - onComplete: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - entityType: PropTypes.object.isRequired, - entityKey: PropTypes.string, - entity: PropTypes.object, -}; - -EmbedSource.defaultProps = { - entity: null, - entityKey: null, -}; - export default EmbedSource; diff --git a/examples/sources/ImageSource.js b/examples/sources/ImageSource.js index bb6d5dff..0d1a3015 100644 --- a/examples/sources/ImageSource.js +++ b/examples/sources/ImageSource.js @@ -1,12 +1,28 @@ +// @flow import React, { Component } from "react"; -import PropTypes from "prop-types"; import { AtomicBlockUtils, EditorState } from "draft-js"; +import type { EntityInstance } from "draft-js"; import Modal from "../components/Modal"; -class ImageSource extends Component { - constructor(props) { +type Props = {| + editorState: EditorState, + onComplete: (EditorState) => void, + onClose: () => void, + entityType: { + type: string, + }, + entity: ?EntityInstance, + entityKey: ?string, +|}; + +type State = {| + src: string, +|}; + +class ImageSource extends Component { + constructor(props: Props) { super(props); const { entity } = this.props; @@ -27,7 +43,8 @@ class ImageSource extends Component { this.onChangeSource = this.onChangeSource.bind(this); } - onConfirm(e) { + /* :: onConfirm: (e: Event) => void; */ + onConfirm(e: Event) { const { editorState, entity, @@ -41,11 +58,13 @@ class ImageSource extends Component { e.preventDefault(); - if (entity) { + if (entity && entityKey) { const nextContent = content.mergeEntityData(entityKey, { src }); nextState = EditorState.push(editorState, nextContent, "apply-entity"); } else { const contentWithEntity = content.createEntity( + // Fixed in https://github.com/facebook/draft-js/commit/6ba124cf663b78c41afd6c361a67bd29724fa617, to be released. + // $FlowFixMe entityType.type, "MUTABLE", { @@ -63,25 +82,34 @@ class ImageSource extends Component { onComplete(nextState); } - onRequestClose(e) { + /* :: onRequestClose: (e: SyntheticEvent<>) => void; */ + onRequestClose(e: SyntheticEvent<>) { const { onClose } = this.props; e.preventDefault(); onClose(); } + /* :: onAfterOpen: () => void; */ onAfterOpen() { - if (this.inputRef) { - this.inputRef.focus(); - this.inputRef.select(); + const input = this.inputRef; + + if (input) { + input.focus(); + input.select(); } } - onChangeSource(e) { - const src = e.target.value; - this.setState({ src }); + /* :: onChangeSource: (e: Event) => void; */ + onChangeSource(e: Event) { + if (e.target instanceof HTMLInputElement) { + const src = e.target.value; + this.setState({ src }); + } } + inputRef: ?HTMLInputElement; + render() { const { src } = this.state; return ( @@ -114,18 +142,4 @@ class ImageSource extends Component { } } -ImageSource.propTypes = { - editorState: PropTypes.object.isRequired, - onComplete: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - entityType: PropTypes.object.isRequired, - entityKey: PropTypes.string, - entity: PropTypes.object, -}; - -ImageSource.defaultProps = { - entity: null, - entityKey: null, -}; - export default ImageSource; diff --git a/examples/sources/LinkSource.js b/examples/sources/LinkSource.js index 9a3e3b9b..fbd1e8a5 100644 --- a/examples/sources/LinkSource.js +++ b/examples/sources/LinkSource.js @@ -1,12 +1,27 @@ +// @flow import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { RichUtils } from "draft-js"; +import { RichUtils, EditorState } from "draft-js"; +import type { EntityInstance } from "draft-js"; import Modal from "../components/Modal"; -class LinkSource extends Component { - constructor(props) { +type Props = {| + editorState: EditorState, + onComplete: (EditorState) => void, + onClose: () => void, + entityType: { + type: string, + }, + entity: ?EntityInstance, +|}; + +type State = {| + url: string, +|}; + +class LinkSource extends Component { + constructor(props: Props) { super(props); const { entity } = this.props; @@ -27,7 +42,8 @@ class LinkSource extends Component { this.onChangeURL = this.onChangeURL.bind(this); } - onConfirm(e) { + /* :: onConfirm: (e: Event) => void; */ + onConfirm(e: Event) { const { editorState, entityType, onComplete } = this.props; const { url } = this.state; @@ -39,6 +55,8 @@ class LinkSource extends Component { url: url.replace(/\s/g, ""), }; const contentStateWithEntity = contentState.createEntity( + // Fixed in https://github.com/facebook/draft-js/commit/6ba124cf663b78c41afd6c361a67bd29724fa617, to be released. + // $FlowFixMe entityType.type, "MUTABLE", data, @@ -53,25 +71,34 @@ class LinkSource extends Component { onComplete(nextState); } - onRequestClose(e) { + /* :: onRequestClose: (e: SyntheticEvent<>) => void; */ + onRequestClose(e: SyntheticEvent<>) { const { onClose } = this.props; e.preventDefault(); onClose(); } + /* :: onAfterOpen: () => void; */ onAfterOpen() { - if (this.inputRef) { - this.inputRef.focus(); - this.inputRef.select(); + const input = this.inputRef; + + if (input) { + input.focus(); + input.select(); } } - onChangeURL(e) { - const url = e.target.value; - this.setState({ url }); + /* :: onChangeURL: (e: Event) => void; */ + onChangeURL(e: Event) { + if (e.target instanceof HTMLInputElement) { + const url = e.target.value; + this.setState({ url }); + } } + inputRef: ?HTMLInputElement; + render() { const { url } = this.state; return ( @@ -102,16 +129,4 @@ class LinkSource extends Component { } } -LinkSource.propTypes = { - editorState: PropTypes.object.isRequired, - onComplete: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - entityType: PropTypes.object.isRequired, - entity: PropTypes.object, -}; - -LinkSource.defaultProps = { - entity: null, -}; - export default LinkSource; diff --git a/examples/utils/polyfills.js b/examples/utils/polyfills.js index 3bffbaaa..a76a7d65 100644 --- a/examples/utils/polyfills.js +++ b/examples/utils/polyfills.js @@ -1,3 +1,4 @@ +// @flow /** * Add all of the required polyfills in this file. */ diff --git a/flow-typed/npm/prismjs_v1.8.4.js b/flow-typed/npm/prismjs_v1.8.4.js new file mode 100644 index 00000000..83a66308 --- /dev/null +++ b/flow-typed/npm/prismjs_v1.8.4.js @@ -0,0 +1,15 @@ +// @flow +// flow-typed signature: 6d127419277bb968ec9f69563a87bd06 +// flow-typed version: <>/prismjs_v1.8.4/flow_v0.89.0 + +declare module "prismjs" { + declare type Token = {| + type: string, + length: number, + |}; + + declare module.exports: { + tokenize: (text: string, grammar: string) => Array, + languages: {}, + }; +} diff --git a/flow-typed/npm/react-component-benchmark_v0.0.4.js b/flow-typed/npm/react-component-benchmark_v0.0.4.js new file mode 100644 index 00000000..95a640fe --- /dev/null +++ b/flow-typed/npm/react-component-benchmark_v0.0.4.js @@ -0,0 +1,50 @@ +// @flow +// flow-typed signature: 28e77c1030280db8981c3735d6829271 +// flow-typed version: <>/react-component-benchmark_v0.0.4/flow_v0.89.0 + +const BenchmarkType = { + MOUNT: "mount", + UPDATE: "update", + UNMOUNT: "unmount", +}; + +declare module "react-component-benchmark" { + declare type FullSampleTimingType = { + start: number, + end: number, + elapsed: number, + }; + + declare export type BenchResultsType = {| + startTime: number, + endTime: number, + runTime: number, + sampleCount: number, + samples: Array, + max: number, + min: number, + median: number, + mean: number, + stdDev: number, + p70: number, + p95: number, + p99: number, + |}; + + declare type Props = {| + component: any, + componentProps?: {}, + onComplete: (x: BenchResultsType) => void, + samples: number, + timeout: number, + type: $Values, + |}; + + declare class Benchmark extends React$Component { + start(): void; + } + + declare export default typeof Benchmark; + + declare export var BenchmarkType: typeof BenchmarkType; +} diff --git a/flow-typed/npm/react-modal_v3.1.x.js b/flow-typed/npm/react-modal_v3.1.x.js new file mode 100644 index 00000000..9b2b35f6 --- /dev/null +++ b/flow-typed/npm/react-modal_v3.1.x.js @@ -0,0 +1,57 @@ +// @flow +// flow-typed signature: 07baeb1a13e7992234dac3ebfeeb3c4f +// flow-typed version: ee87414e77/react-modal_v3.1.x/flow_>=v0.54.1 + +declare module "react-modal" { + declare type DefaultProps = { + isOpen?: boolean, + portalClassName?: string, + bodyOpenClassName?: string, + ariaHideApp?: boolean, + closeTimeoutMS?: number, + shouldFocusAfterRender?: boolean, + shouldCloseOnEsc?: boolean, + shouldCloseOnOverlayClick?: boolean, + shouldReturnFocusAfterClose?: boolean, + parentSelector?: () => HTMLElement, + }; + + declare type Props = DefaultProps & { + style?: { + content?: { + [key: string]: string | number, + }, + overlay?: { + [key: string]: string | number, + }, + }, + className?: + | string + | { + base: string, + afterOpen: string, + beforeClose: string, + }, + overlayClassName?: + | string + | { + base: string, + afterOpen: string, + beforeClose: string, + }, + appElement?: HTMLElement | string | null, + onAfterOpen?: () => void | Promise, + onRequestClose?: (SyntheticEvent<>) => void, + aria?: { + [key: string]: string, + }, + role?: string, + contentLabel?: string, + }; + + declare class Modal extends React$Component { + static setAppElement(element: HTMLElement | string | null): void; + } + + declare module.exports: typeof Modal; +} diff --git a/flow-typed/npm/reading-time_v1.1.0.js b/flow-typed/npm/reading-time_v1.1.0.js new file mode 100644 index 00000000..3aef8ee0 --- /dev/null +++ b/flow-typed/npm/reading-time_v1.1.0.js @@ -0,0 +1,12 @@ +// @flow +// flow-typed signature: aca02e7bfed6dedd7e76df06d5247aec +// flow-typed version: <>/reading-time_v1.1.0/flow_v0.89.0 + +declare module "reading-time" { + declare module.exports: (string) => {| + text: string, + minutes: number, + time: number, + words: number, + |}; +} diff --git a/flow-typed/npm/stats-js_v1.0.0.js b/flow-typed/npm/stats-js_v1.0.0.js new file mode 100644 index 00000000..12262b4d --- /dev/null +++ b/flow-typed/npm/stats-js_v1.0.0.js @@ -0,0 +1,14 @@ +// @flow +// flow-typed signature: 754639b42acabb56189cf2f613dc9edf +// flow-typed version: <>/stats-js_v1.0.0/flow_v0.89.0 + +declare module "stats-js" { + declare class Stats { + showPanel: (1 | 2 | 3 | 4) => void; + begin: () => void; + update: () => void; + domElement: Element; + } + + declare module.exports: typeof Stats; +} diff --git a/flow-typed/webpack_globals.js b/flow-typed/webpack_globals.js new file mode 100644 index 00000000..1a2e4c44 --- /dev/null +++ b/flow-typed/webpack_globals.js @@ -0,0 +1,12 @@ +// @flow +declare var EMBEDLY_API_KEY: string | typeof undefined; +declare var PKG_VERSION: string | typeof undefined; +declare var SVG_ICONS: string | typeof undefined; +declare var SENTRY_DSN: string | typeof undefined; +declare var process: + | { + env: { + NODE_ENV: string, + }, + } + | typeof undefined; diff --git a/lib/api/DraftUtils.js b/lib/api/DraftUtils.js index d3a02c62..93cf4842 100644 --- a/lib/api/DraftUtils.js +++ b/lib/api/DraftUtils.js @@ -1,10 +1,13 @@ +// @flow import { EditorState, + ContentState, Modifier, AtomicBlockUtils, RichUtils, SelectionState, } from "draft-js"; +import type { ContentBlock } from "draft-js"; import isSoftNewlineEvent from "draft-js/lib/isSoftNewlineEvent"; import { BLOCK_TYPE, ENTITY_TYPE } from "./constants"; @@ -19,7 +22,7 @@ export default { /** * Returns the first selected block. */ - getSelectedBlock(editorState) { + getSelectedBlock(editorState: EditorState) { const selection = editorState.getSelection(); const content = editorState.getCurrentContent(); @@ -31,7 +34,7 @@ export default { * An entity can not span multiple blocks. * https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/inline.js#L75 */ - getSelectionEntity(editorState) { + getSelectionEntity(editorState: EditorState) { let entity; const selection = editorState.getSelection(); let start = selection.getStartOffset(); @@ -63,7 +66,7 @@ export default { * Creates a selection on a given entity in the currently selected block. * Returns the current selection if no entity key is provided, or if the entity could not be found. */ - getEntitySelection(editorState, entityKey) { + getEntitySelection(editorState: EditorState, entityKey: string) { const selection = editorState.getSelection(); if (!entityKey) { @@ -96,7 +99,7 @@ export default { /** * Updates a given atomic block's entity, merging new data with the old one. */ - updateBlockEntity(editorState, block, data) { + updateBlockEntity(editorState: EditorState, block: ContentBlock, data: {}) { const content = editorState.getCurrentContent(); let nextContent = content.mergeEntityData(block.getEntityAt(0), data); @@ -121,9 +124,12 @@ export default { * Returns updated EditorState. * Inspired by DraftUtils.addLineBreakRemovingSelection. */ - addHorizontalRuleRemovingSelection(editorState) { + addHorizontalRuleRemovingSelection(editorState: EditorState) { const contentState = editorState.getCurrentContent(); const contentStateWithEntity = contentState.createEntity( + // Draft.js Flow typing issue. + // See https://github.com/facebook/draft-js/issues/868. + // $FlowFixMe ENTITY_TYPE.HORIZONTAL_RULE, "IMMUTABLE", {}, @@ -138,7 +144,11 @@ export default { * Also removes the required characters from the characterList, * and resets block data. */ - resetBlockWithType(editorState, newType, newText) { + resetBlockWithType( + editorState: EditorState, + newType: string, + newText: string, + ) { const contentState = editorState.getCurrentContent(); const selectionState = editorState.getSelection(); const key = selectionState.getStartKey(); @@ -180,7 +190,7 @@ export default { /** * Removes the block at the given key. */ - removeBlock(editorState, key) { + removeBlock(editorState: EditorState, key: string) { const content = editorState.getCurrentContent(); const blockMap = content.getBlockMap().remove(key); @@ -195,7 +205,11 @@ export default { * Removes a block-level entity, turning the block into an empty paragraph, * and placing the selection on it. */ - removeBlockEntity(editorState, entityKey, blockKey) { + removeBlockEntity( + editorState: EditorState, + entityKey: string, + blockKey: string, + ) { let newState = editorState; const content = editorState.getCurrentContent(); @@ -232,7 +246,7 @@ export default { * Ideally this should be handled by the built-in RichUtils, but it's not. * See https://github.com/wagtail/wagtail/issues/4370. */ - handleDeleteAtomic(editorState) { + handleDeleteAtomic(editorState: EditorState) { const selection = editorState.getSelection(); const content = editorState.getCurrentContent(); const key = selection.getAnchorKey(); @@ -255,9 +269,13 @@ export default { * Get an entity decorator strategy based on the given entity type. * This strategy will find all entities of the given type. */ - getEntityTypeStrategy(entityType) { - const strategy = (contentBlock, callback, contentState) => { - contentBlock.findEntityRanges((character) => { + getEntityTypeStrategy(entityType: string) { + const strategy = ( + block: ContentBlock, + callback: (start: number, end: number) => void, + contentState: ContentState, + ) => { + block.findEntityRanges((character) => { const entityKey = character.getEntity(); return ( entityKey !== null && @@ -274,7 +292,7 @@ export default { * See https://draftjs.org/docs/api-reference-editor.html#placeholder * for details on why this is useful. */ - shouldHidePlaceholder(editorState) { + shouldHidePlaceholder(editorState: EditorState) { const contentState = editorState.getCurrentContent(); return ( contentState.hasText() || @@ -291,7 +309,7 @@ export default { * but changed so that the split + block type reset amounts to * only one change in the undo stack. */ - insertNewUnstyledBlock(editorState) { + insertNewUnstyledBlock(editorState: EditorState) { const selection = editorState.getSelection(); let newContent = Modifier.splitBlock( editorState.getCurrentContent(), @@ -316,7 +334,7 @@ export default { * Handles Shift + Enter keypress removing selection and inserting a line break. * https://github.com/jpuri/draftjs-utils/blob/112bbe449cc9156522fcf2b40f2910a071b795c2/js/block.js#L133 */ - addLineBreak(editorState) { + addLineBreak(editorState: EditorState) { const content = editorState.getCurrentContent(); const selection = editorState.getSelection(); @@ -341,7 +359,7 @@ export default { * Handles hard newlines. * https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/keyPress.js#L17 */ - handleHardNewline(editorState) { + handleHardNewline(editorState: EditorState) { const selection = editorState.getSelection(); if (!selection.isCollapsed()) { @@ -369,11 +387,12 @@ export default { const depth = block.getDepth(); if (depth === 0) { - return EditorState.push( - editorState, - RichUtils.tryToRemoveBlockStyle(editorState), - "change-block-type", - ); + const nextContent = RichUtils.tryToRemoveBlockStyle(editorState); + // At the moment, tryToRemoveBlockStyle always returns for + // collapsed selections at the start of a block. So in theory this corner case should never happen. + return nextContent + ? EditorState.push(editorState, nextContent, "change-block-type") + : false; } const blockMap = content.getBlockMap(); @@ -399,7 +418,7 @@ export default { * See https://github.com/springload/draftail/issues/104, * https://github.com/jpuri/draftjs-utils/issues/10. */ - handleNewLine(editorState, event) { + handleNewLine(editorState: EditorState, event: SyntheticKeyboardEvent<>) { // https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/keyPress.js#L64 if (isSoftNewlineEvent(event)) { return this.addLineBreak(editorState); diff --git a/lib/api/DraftUtils.test.js b/lib/api/DraftUtils.test.js index 2350c13b..42b08ad7 100644 --- a/lib/api/DraftUtils.test.js +++ b/lib/api/DraftUtils.test.js @@ -769,10 +769,25 @@ describe("DraftUtils", () => { }); const editorState = EditorState.createWithContent(contentState); DraftUtils.handleHardNewline(editorState); - expect(RichUtils.tryToRemoveBlockStyle).toHaveBeenCalled(); }); + it("empty list block non-nested, tryToRemoveBlockStyle fail", () => { + const contentState = convertFromRaw({ + entityMap: {}, + blocks: [ + { + key: "b0ei9", + text: "", + type: "unordered-list-item", + }, + ], + }); + const editorState = EditorState.createWithContent(contentState); + RichUtils.tryToRemoveBlockStyle.mockImplementationOnce(() => null); + expect(DraftUtils.handleHardNewline(editorState)).toBe(false); + }); + it("empty list block nested", () => { const contentState = convertFromRaw({ entityMap: {}, diff --git a/lib/api/behavior.js b/lib/api/behavior.js index 83fb8e1a..b121e042 100644 --- a/lib/api/behavior.js +++ b/lib/api/behavior.js @@ -1,8 +1,11 @@ +// @flow import { DefaultDraftBlockRenderMap, getDefaultKeyBinding, KeyBindingUtil, + EditorState, } from "draft-js"; +import type { ContentBlock } from "draft-js"; import { filterEditorState } from "draftjs-filters"; import { blockDepthStyleFn } from "draftjs-conductor"; @@ -22,6 +25,7 @@ const hasCmd = hasCommandModifier; // Hack relying on the internals of Draft.js. // See https://github.com/facebook/draft-js/pull/869 +// $FlowFixMe const IS_MAC_OS = isOptionKeyCommand({ altKey: "test" }) === "test"; /** @@ -31,7 +35,9 @@ export default { /** * Configure block render map from block types list. */ - getBlockRenderMap(blockTypes) { + getBlockRenderMap( + blockTypes: $ReadOnlyArray<{ type: string, element?: string }>, + ) { let renderMap = DefaultDraftBlockRenderMap; // Override default element for code block. @@ -43,11 +49,13 @@ export default { }); } - blockTypes.filter((block) => block.element).forEach((block) => { - renderMap = renderMap.set(block.type, { - element: block.element, + blockTypes + .filter((block) => block.element) + .forEach((block) => { + renderMap = renderMap.set(block.type, { + element: block.element, + }); }); - }); return renderMap; }, @@ -55,7 +63,7 @@ export default { /** * block style function automatically adding a class with the block's type. */ - blockStyleFn(block) { + blockStyleFn(block: ContentBlock) { const type = block.getType(); return `Draftail-block--${type} ${blockDepthStyleFn(block)}`; @@ -64,7 +72,11 @@ export default { /** * Configure key binding function from enabled blocks, styles, entities. */ - getKeyBindingFn(blockTypes, inlineStyles, entityTypes) { + getKeyBindingFn( + blockTypes: $ReadOnlyArray<{ type: string }>, + inlineStyles: $ReadOnlyArray<{ type: string }>, + entityTypes: $ReadOnlyArray<{ type: string }>, + ) { const getEnabled = (activeTypes) => activeTypes.reduce((enabled, type) => { enabled[type.type] = type.type; @@ -76,7 +88,7 @@ export default { const entities = getEnabled(entityTypes); // Emits key commands to use in `handleKeyCommand` in `Editor`. - const keyBindingFn = (e) => { + const keyBindingFn = (e: SyntheticKeyboardEvent<>) => { // Safeguard that we only trigger shortcuts with exact matches. // eg. cmd + shift + b should not trigger bold. if (e.shiftKey) { @@ -142,11 +154,11 @@ export default { return keyBindingFn; }, - hasKeyboardShortcut(type) { + hasKeyboardShortcut(type: string) { return !!KEYBOARD_SHORTCUTS[type]; }, - getKeyboardShortcut(type, isMacOS = IS_MAC_OS) { + getKeyboardShortcut(type: string, isMacOS: boolean = IS_MAC_OS) { const shortcut = KEYBOARD_SHORTCUTS[type]; const system = isMacOS ? "macOS" : "other"; @@ -160,20 +172,25 @@ export default { * * Returns the new block type, or false if no replacement should occur. */ - handleBeforeInputBlockType(mark, blockTypes) { + handleBeforeInputBlockType( + mark: string, + blockTypes: $ReadOnlyArray<{ type: string }>, + ) { return blockTypes.find((b) => b.type === INPUT_BLOCK_MAP[mark]) ? INPUT_BLOCK_MAP[mark] : false; }, - handleBeforeInputHR(mark, block) { + handleBeforeInputHR(mark: string, block: ContentBlock) { return ( mark === INPUT_ENTITY_MAP[ENTITY_TYPE.HORIZONTAL_RULE] && block.getType() !== BLOCK_TYPE.CODE ); }, - getCustomStyleMap(inlineStyles) { + getCustomStyleMap( + inlineStyles: $ReadOnlyArray<{ type: string, style?: {} }>, + ) { const customStyleMap = {}; inlineStyles.forEach((style) => { @@ -202,8 +219,21 @@ export default { blockTypes, inlineStyles, entityTypes, + }: { + maxListNesting: number, + enableHorizontalRule: boolean | {}, + enableLineBreak: boolean | {}, + blockTypes: $ReadOnlyArray<{ type: string }>, + inlineStyles: $ReadOnlyArray<{ type: string }>, + entityTypes: $ReadOnlyArray<{ + type: string, + attributes?: $ReadOnlyArray, + whitelist?: { + [attribute: string]: string | boolean, + }, + }>, }, - editorState, + editorState: EditorState, ) { const enabledEntityTypes = entityTypes.slice(); const whitespacedCharacters = ["\t", "📷"]; diff --git a/lib/api/constants.js b/lib/api/constants.js index b4a9dd5a..39328086 100644 --- a/lib/api/constants.js +++ b/lib/api/constants.js @@ -1,3 +1,4 @@ +// @flow import { DefaultDraftInlineStyle } from "draft-js"; // See https://github.com/facebook/draft-js/blob/master/src/model/immutable/DefaultDraftBlockRenderMap.js @@ -124,15 +125,15 @@ export const KEY_CODES = { J: 74, I: 73, X: 88, - 0: 48, - 1: 49, - 2: 50, - 3: 51, - 4: 52, - 5: 53, - 6: 54, - 7: 55, - 8: 56, + "0": 48, + "1": 49, + "2": 50, + "3": 51, + "4": 52, + "5": 53, + "6": 54, + "7": 55, + "8": 56, ".": 190, ",": 188, }; diff --git a/lib/api/conversion.js b/lib/api/conversion.js index cb391966..25094f5f 100644 --- a/lib/api/conversion.js +++ b/lib/api/conversion.js @@ -1,16 +1,27 @@ +// @flow import { EditorState, convertFromRaw, convertToRaw, CompositeDecorator, } from "draft-js"; +import type { RawDraftContentState } from "draft-js/lib/RawDraftContentState"; +import type { DraftDecorator } from "draft-js/lib/DraftDecorator"; +import type { DraftDecoratorType } from "draft-js/lib/DraftDecoratorType"; const EMPTY_CONTENT_STATE = null; export default { - // RawDraftContentState + decorators => EditorState. - createEditorState(rawContentState, decorators) { - const compositeDecorator = new CompositeDecorator(decorators); + createEditorState( + rawContentState: ?RawDraftContentState, + decorators: Array, + ) { + // Draft.js flow types are inconsistent with the documented usage of this API. + // See https://github.com/facebook/draft-js/issues/1585. + // $FlowFixMe + const compositeDecorator: DraftDecoratorType = new CompositeDecorator( + decorators, + ); let editorState; if (rawContentState) { @@ -26,17 +37,17 @@ export default { return editorState; }, - // EditorState => RawDraftContentState. - serialiseEditorState(editorState) { + serialiseEditorState(editorState: EditorState) { const contentState = editorState.getCurrentContent(); const rawContentState = convertToRaw(contentState); - const isEmpty = rawContentState.blocks.every( - (block) => + const isEmpty = rawContentState.blocks.every((block) => { + const isEmptyBlock = block.text.trim().length === 0 && - block.entityRanges.length === 0 && - block.inlineStyleRanges.length === 0, - ); + (!block.entityRanges || block.entityRanges.length === 0) && + (!block.inlineStyleRanges || block.inlineStyleRanges.length === 0); + return isEmptyBlock; + }); return isEmpty ? EMPTY_CONTENT_STATE : rawContentState; }, diff --git a/lib/blocks/DividerBlock.js b/lib/blocks/DividerBlock.js index ab6487a0..5949eebe 100644 --- a/lib/blocks/DividerBlock.js +++ b/lib/blocks/DividerBlock.js @@ -1,3 +1,4 @@ +// @flow import React from "react"; /** diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index e8e06bcd..fbd0d055 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -1,6 +1,11 @@ -import PropTypes from "prop-types"; +// @flow import React, { Component } from "react"; -import { Editor, EditorState, RichUtils } from "draft-js"; +import type { ComponentType } from "react"; +import { Editor, EditorState, RichUtils, ContentBlock } from "draft-js"; +import type { EntityInstance } from "draft-js"; +import type { RawDraftContentState } from "draft-js/lib/RawDraftContentState"; +import type { DraftEditorCommand } from "draft-js/lib/DraftEditorCommand"; +import type { DraftDecorator } from "draft-js/lib/DraftDecorator"; import { ListNestingStyles, registerCopySource, @@ -26,15 +31,166 @@ import conversion from "../api/conversion"; import getComponentWrapper from "../utils/getComponentWrapper"; import Toolbar from "./Toolbar"; +import type { IconProp } from "./Icon"; import DividerBlock from "../blocks/DividerBlock"; +type ControlProp = {| + // Describes the control in the editor UI, concisely. + label?: string, + // Describes the control in the editor UI. + description?: string, + // Represents the control in the editor UI. + icon?: IconProp, +|}; + +type Props = {| + rawContentState: ?RawDraftContentState, + onSave: ?(content: null | RawDraftContentState) => void, + onFocus: ?() => void, + onBlur: ?() => void, + placeholder: ?string, + enableHorizontalRule: boolean | ControlProp, + enableLineBreak: boolean | ControlProp, + showUndoControl: boolean | ControlProp, + showRedoControl: boolean | ControlProp, + stripPastedStyles: boolean, + spellCheck: boolean, + textAlignment: ?string, + textDirectionality: ?string, + autoCapitalize: ?string, + autoComplete: ?string, + autoCorrect: ?string, + ariaDescribedBy: ?string, + blockTypes: $ReadOnlyArray<{| + ...ControlProp, + // Unique type shared between block instances. + type: string, + // DOM element used to display the block within the editor area. + element?: string, + |}>, + inlineStyles: $ReadOnlyArray<{| + ...ControlProp, + // Unique type shared between inline style instances. + type: string, + // CSS properties (in JS format) to apply for styling within the editor area. + style?: {}, + |}>, + entityTypes: $ReadOnlyArray<{| + ...ControlProp, + // Unique type shared between entity instances. + type: string, + // React component providing the UI to manage entities of this type. + source: ComponentType<{}>, + // React component to display inline entities. + decorator?: ComponentType<{}>, + // React component to display block-level entities. + block?: ComponentType<{}>, + // Array of attributes the entity uses, to preserve when filtering entities on paste. + // If undefined, all entity data is preserved. + attributes?: $ReadOnlyArray, + // Attribute - regex mapping, to whitelist entities based on their data on paste. + // For example, { url: '^https:' } will only preserve links that point to HTTPS URLs. + whitelist?: {}, + |}>, + decorators: $ReadOnlyArray, + // Additional React components to render in the toolbar. + controls: $ReadOnlyArray< + ComponentType<{| + getEditorState: () => EditorState, + onChange: (EditorState) => void, + |}>, + >, + maxListNesting: number, + stateSaveInterval: number, +|}; + +const defaultProps = { + // Initial content of the editor. Use this to edit pre-existing content. + rawContentState: null, + // Called when changes occured. Use this to persist editor content. + onSave: null, + // Called when the editor receives focus. + onFocus: null, + // Called when the editor loses focus. + onBlur: null, + // Displayed when the editor is empty. Hidden if the user changes styling. + placeholder: null, + // Enable the use of horizontal rules in the editor. + enableHorizontalRule: false, + // Enable the use of line breaks in the editor. + enableLineBreak: false, + // Show undo control in the toolbar. + showUndoControl: false, + // Show redo control in the toolbar. + showRedoControl: false, + // Disable copy/paste of rich text in the editor. + stripPastedStyles: true, + // Set whether spellcheck is turned on for your editor. + // See https://draftjs.org/docs/api-reference-editor.html#spellcheck. + spellCheck: false, + // Optionally set the overriding text alignment for this editor. + // See https://draftjs.org/docs/api-reference-editor.html#textalignment. + textAlignment: null, + // Optionally set the overriding text directionality for this editor. + // See https://draftjs.org/docs/api-reference-editor.html#textdirectionality. + textDirectionality: null, + // Set if auto capitalization is turned on and how it behaves. + // See https://draftjs.org/docs/api-reference-editor.html#autocapitalize-string. + autoCapitalize: null, + // Set if auto complete is turned on and how it behaves. + // See https://draftjs.org/docs/api-reference-editor.html#autocomplete-string. + autoComplete: null, + // Set if auto correct is turned on and how it behaves. + // See https://draftjs.org/docs/api-reference-editor.html#autocorrect-string. + autoCorrect: null, + // See https://draftjs.org/docs/api-reference-editor.html#aria-props. + ariaDescribedBy: null, + // List of the available block types. + blockTypes: [], + // List of the available inline styles. + inlineStyles: [], + // List of the available entity types. + entityTypes: [], + // List of active decorators. + decorators: [], + // List of extra toolbar controls. + controls: [], + // Max level of nesting for list items. 0 = no nesting. Maximum = 10. + maxListNesting: 1, + // Frequency at which to call the save callback (ms). + stateSaveInterval: 250, +}; + +type State = {| + editorState: EditorState, + hasFocus: boolean, + readOnly: boolean, + sourceOptions: ?{ + entity: ?EntityInstance, + entityKey: ?string, + entityType: ?{ + source: ComponentType<{}>, + }, + }, +|}; + +/* :: import type { ElementRef, Node } from "react"; */ + /** * Main component of the Draftail editor. * Contains the Draft.js editor instance, and ties together UI and behavior. */ -class DraftailEditor extends Component { - constructor(props) { +class DraftailEditor extends Component { + static defaultProps: Props; + + /* :: editorRef: ElementRef; */ + /* :: copySource: { unregister: () => void }; */ + /* :: updateTimeout: ?number; */ + /* :: lockEditor: () => void; */ + /* :: unlockEditor: () => void; */ + + constructor(props: Props) { super(props); this.onChange = this.onChange.bind(this); @@ -79,6 +235,7 @@ class DraftailEditor extends Component { .filter((type) => !!type.decorator) .map((type) => ({ strategy: DraftUtils.getEntityTypeStrategy(type.type), + // $FlowFixMe component: getComponentWrapper(type.decorator, { onEdit: this.onEditEntity, onRemove: this.onRemoveEntity, @@ -104,6 +261,7 @@ class DraftailEditor extends Component { this.copySource.unregister(); } + /* :: onFocus: () => void; */ onFocus() { this.setState({ hasFocus: true, @@ -116,6 +274,7 @@ class DraftailEditor extends Component { } } + /* :: onBlur: () => void; */ onBlur() { this.setState({ hasFocus: false, @@ -128,7 +287,8 @@ class DraftailEditor extends Component { } } - onTab(event) { + /* :: onTab: (event: SyntheticKeyboardEvent<>) => true; */ + onTab(event: SyntheticKeyboardEvent<>) { const { maxListNesting } = this.props; const { editorState } = this.state; const newState = RichUtils.onTab(event, editorState, maxListNesting); @@ -137,7 +297,8 @@ class DraftailEditor extends Component { return true; } - onChange(nextState) { + /* :: onChange: (nextState: EditorState) => void; */ + onChange(nextState: EditorState) { const { stateSaveInterval, maxListNesting, @@ -181,13 +342,15 @@ class DraftailEditor extends Component { ); } - onEditEntity(entityKey) { + /* :: onEditEntity: (entityKey: string) => void; */ + onEditEntity(entityKey: string) { const { entityTypes } = this.props; const { editorState } = this.state; const content = editorState.getCurrentContent(); const entity = content.getEntity(entityKey); const entityType = entityTypes.find((t) => t.type === entity.type); + // $FlowFixMe if (!entityType.block) { const entitySelection = DraftUtils.getEntitySelection( editorState, @@ -204,7 +367,8 @@ class DraftailEditor extends Component { this.toggleSource(entity.getType(), entityKey, entity); } - onRemoveEntity(entityKey, blockKey) { + /* :: onRemoveEntity: (entityKey: string, blockKey: string) => void; */ + onRemoveEntity(entityKey: string, blockKey: string) { const { entityTypes } = this.props; const { editorState } = this.state; const content = editorState.getCurrentContent(); @@ -212,6 +376,7 @@ class DraftailEditor extends Component { const entityType = entityTypes.find((t) => t.type === entity.type); let newState = editorState; + // $FlowFixMe if (entityType.block) { newState = DraftUtils.removeBlockEntity(newState, entityKey, blockKey); } else { @@ -226,7 +391,8 @@ class DraftailEditor extends Component { this.onChange(newState); } - onUndoRedo(type) { + /* :: onUndoRedo: (type: string) => void; */ + onUndoRedo(type: string) { const { editorState } = this.state; let newEditorState = editorState; @@ -239,7 +405,8 @@ class DraftailEditor extends Component { this.onChange(newEditorState); } - onRequestSource(entityType) { + /* :: onRequestSource: (entityType: string) => void; */ + onRequestSource(entityType: string) { const { editorState } = this.state; const contentState = editorState.getCurrentContent(); const entityKey = DraftUtils.getSelectionEntity(editorState); @@ -251,7 +418,8 @@ class DraftailEditor extends Component { ); } - onCompleteSource(nextState) { + /* :: onCompleteSource: (nextState: EditorState) => void; */ + onCompleteSource(nextState: EditorState) { this.setState( { sourceOptions: null, @@ -272,6 +440,7 @@ class DraftailEditor extends Component { ); } + /* :: onCloseSource: () => void; */ onCloseSource() { this.setState({ sourceOptions: null, @@ -279,25 +448,31 @@ class DraftailEditor extends Component { }); } + /* :: getEditorState: () => EditorState; */ getEditorState() { const { editorState } = this.state; return editorState; } + /* :: saveState: () => void; */ saveState() { const { onSave } = this.props; const { editorState } = this.state; - onSave(conversion.serialiseEditorState(editorState)); + if (onSave) { + onSave(conversion.serialiseEditorState(editorState)); + } } - toggleEditor(readOnly) { + /* :: toggleEditor: (readOnly: boolean) => void; */ + toggleEditor(readOnly: boolean) { this.setState({ readOnly, }); } - toggleSource(type, entityKey, entity) { + /* :: toggleSource: (type:string, entityKey: ?string, entity: ?EntityInstance) => void; */ + toggleSource(type: string, entityKey: ?string, entity: ?EntityInstance) { const { entityTypes } = this.props; const entityType = entityTypes.find((item) => item.type === type); @@ -311,7 +486,8 @@ class DraftailEditor extends Component { }); } - handleReturn(e) { + /* :: handleReturn: (e: SyntheticKeyboardEvent<>) => void; */ + handleReturn(e: SyntheticKeyboardEvent<>) { const { enableLineBreak } = this.props; const { editorState } = this.state; const contentState = editorState.getCurrentContent(); @@ -349,7 +525,8 @@ class DraftailEditor extends Component { return ret; } - handleKeyCommand(command) { + /* :: handleKeyCommand: (command: DraftEditorCommand) => boolean; */ + handleKeyCommand(command: DraftEditorCommand) { const { editorState } = this.state; if (ENTITY_TYPES.includes(command)) { @@ -386,7 +563,8 @@ class DraftailEditor extends Component { return false; } - handleBeforeInput(char) { + /* :: handleBeforeInput: (char: string) => 'handled' | 'not-handled'; */ + handleBeforeInput(char: string) { const { blockTypes, enableHorizontalRule } = this.props; const { editorState } = this.state; const selection = editorState.getSelection(); @@ -428,7 +606,8 @@ class DraftailEditor extends Component { return NOT_HANDLED; } - handlePastedText(text, html, editorState) { + /* :: handlePastedText: (text: string, html: ?string, editorState: EditorState) => boolean; */ + handlePastedText(text: string, html: ?string, editorState: EditorState) { const { stripPastedStyles } = this.props; // Leave paste handling to Draft.js when stripping styles is desirable. @@ -446,27 +625,32 @@ class DraftailEditor extends Component { return false; } - toggleBlockType(blockType) { + /* :: toggleBlockType: (blockType: string) => void; */ + toggleBlockType(blockType: string) { const { editorState } = this.state; this.onChange(RichUtils.toggleBlockType(editorState, blockType)); } - toggleInlineStyle(inlineStyle) { + /* :: toggleInlineStyle: (inlineStyle: string) => void; */ + toggleInlineStyle(inlineStyle: string) { const { editorState } = this.state; this.onChange(RichUtils.toggleInlineStyle(editorState, inlineStyle)); } + /* :: addHR: () => void; */ addHR() { const { editorState } = this.state; this.onChange(DraftUtils.addHorizontalRuleRemovingSelection(editorState)); } + /* :: addBR: () => void; */ addBR() { const { editorState } = this.state; this.onChange(DraftUtils.addLineBreak(editorState)); } - blockRenderer(block) { + /* :: blockRenderer: (block: ContentBlock) => {}; */ + blockRenderer(block: ContentBlock) { const { entityTypes } = this.props; const { editorState } = this.state; const contentState = editorState.getCurrentContent(); @@ -496,6 +680,7 @@ class DraftailEditor extends Component { const entityType = entityTypes.find((t) => t.type === entity.type); return { + // $FlowFixMe component: entityType.block, editable: false, props: { @@ -527,10 +712,12 @@ class DraftailEditor extends Component { // Imperative focus API similar to that of Draft.js. // See https://draftjs.org/docs/advanced-topics-managing-focus.html#content. + /* :: focus: () => void; */ focus() { this.editorRef.focus(); } + /* :: renderSource: () => ?Node; */ renderSource() { const { editorState, sourceOptions } = this.state; @@ -654,216 +841,6 @@ class DraftailEditor extends Component { } } -DraftailEditor.defaultProps = { - // Initial content of the editor. Use this to edit pre-existing content. - rawContentState: null, - // Called when changes occured. Use this to persist editor content. - onSave: () => {}, - // Called when the editor receives focus. - onFocus: null, - // Called when the editor loses focus. - onBlur: null, - // Displayed when the editor is empty. Hidden if the user changes styling. - placeholder: null, - // Enable the use of horizontal rules in the editor. - enableHorizontalRule: false, - // Enable the use of line breaks in the editor. - enableLineBreak: false, - // Show undo control in the toolbar. - showUndoControl: false, - // Show redo control in the toolbar. - showRedoControl: false, - // Disable copy/paste of rich text in the editor. - stripPastedStyles: true, - // Set whether spellcheck is turned on for your editor. - // See https://draftjs.org/docs/api-reference-editor.html#spellcheck. - spellCheck: false, - // Optionally set the overriding text alignment for this editor. - // See https://draftjs.org/docs/api-reference-editor.html#textalignment. - textAlignment: null, - // Optionally set the overriding text directionality for this editor. - // See https://draftjs.org/docs/api-reference-editor.html#textdirectionality. - textDirectionality: null, - // Set if auto capitalization is turned on and how it behaves. - // See https://draftjs.org/docs/api-reference-editor.html#autocapitalize-string. - autoCapitalize: null, - // Set if auto complete is turned on and how it behaves. - // See https://draftjs.org/docs/api-reference-editor.html#autocomplete-string. - autoComplete: null, - // Set if auto correct is turned on and how it behaves. - // See https://draftjs.org/docs/api-reference-editor.html#autocorrect-string. - autoCorrect: null, - // See https://draftjs.org/docs/api-reference-editor.html#aria-props. - ariaDescribedBy: null, - // List of the available block types. - blockTypes: [], - // List of the available inline styles. - inlineStyles: [], - // List of the available entity types. - entityTypes: [], - // List of active decorators. - decorators: [], - // List of extra toolbar controls. - controls: [], - // Max level of nesting for list items. 0 = no nesting. Maximum = 10. - maxListNesting: 1, - // Frequency at which to call the save callback (ms). - stateSaveInterval: 250, -}; - -DraftailEditor.propTypes = { - rawContentState: PropTypes.object, - onSave: PropTypes.func, - onFocus: PropTypes.func, - onBlur: PropTypes.func, - placeholder: PropTypes.string, - enableHorizontalRule: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]), - enableLineBreak: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]), - showUndoControl: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]), - showRedoControl: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]), - stripPastedStyles: PropTypes.bool, - spellCheck: PropTypes.bool, - textAlignment: PropTypes.string, - textDirectionality: PropTypes.string, - autoCapitalize: PropTypes.string, - autoComplete: PropTypes.string, - autoCorrect: PropTypes.string, - ariaDescribedBy: PropTypes.string, - blockTypes: PropTypes.arrayOf( - PropTypes.shape({ - // Unique type shared between block instances. - type: PropTypes.string.isRequired, - // Describes the block in the editor UI, concisely. - label: PropTypes.string, - // Describes the block in the editor UI. - description: PropTypes.string, - // Represents the block in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - // DOM element used to display the block within the editor area. - element: PropTypes.string, - }), - ), - inlineStyles: PropTypes.arrayOf( - PropTypes.shape({ - // Unique type shared between inline style instances. - type: PropTypes.string.isRequired, - // Describes the inline style in the editor UI, concisely. - label: PropTypes.string, - // Describes the inline style in the editor UI. - description: PropTypes.string, - // Represents the inline style in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - // CSS properties (in JS format) to apply for styling within the editor area. - style: PropTypes.Object, - }), - ), - entityTypes: PropTypes.arrayOf( - PropTypes.shape({ - // Unique type shared between entity instances. - type: PropTypes.string.isRequired, - // Describes the entity in the editor UI, concisely. - label: PropTypes.string, - // Describes the entity in the editor UI. - description: PropTypes.string, - // Represents the entity in the editor UI. - icon: PropTypes.oneOfType([ - // String icon = SVG path or symbol reference. - PropTypes.string, - // List of SVG paths. - PropTypes.arrayOf(PropTypes.string), - // Arbitrary React element. - PropTypes.node, - ]), - // React component providing the UI to manage entities of this type. - source: PropTypes.func.isRequired, - // React component to display inline entities. - decorator: PropTypes.func, - // React component to display block-level entities. - block: PropTypes.func, - // Array of attributes the entity uses, to preserve when filtering entities on paste. - // If undefined, all entity data is preserved. - attributes: PropTypes.arrayOf(PropTypes.string), - // Attribute - regex mapping, to whitelist entities based on their data on paste. - // For example, { url: '^https:' } will only preserve links that point to HTTPS URLs. - whitelist: PropTypes.object, - }), - ), - decorators: PropTypes.arrayOf( - PropTypes.shape({ - // Determines which pieces of content are to be decorated. - strategy: PropTypes.func, - // React component to display the decoration. - component: PropTypes.func, - }), - ), - // Additional React components to render in the toolbar. - controls: PropTypes.arrayOf(PropTypes.func), - maxListNesting: PropTypes.number, - stateSaveInterval: PropTypes.number, -}; +DraftailEditor.defaultProps = defaultProps; export default DraftailEditor; diff --git a/lib/components/Icon.js b/lib/components/Icon.js index 64825d9d..24323828 100644 --- a/lib/components/Icon.js +++ b/lib/components/Icon.js @@ -1,14 +1,22 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; +import type { Node } from "react"; + +export type IconProp = string | string[] | Node; + +type Props = {| + icon: IconProp, + title: ?string, + className: ?string, +|}; /** * Icon as SVG element. Can optionally render a React element instead. */ -const Icon = ({ icon, title, className }) => { - const isPathOrRef = typeof icon === "string"; +const Icon = ({ icon, title, className }: Props) => { let children; - if (isPathOrRef) { + if (typeof icon === "string") { if (icon.includes("#")) { children = ; } else { @@ -36,20 +44,6 @@ const Icon = ({ icon, title, className }) => { ); }; -Icon.propTypes = { - // The icon definition is very flexible. - icon: PropTypes.oneOfType([ - // String icon = SVG path or symbol reference. - PropTypes.string, - // List of SVG paths. - PropTypes.arrayOf(PropTypes.string), - // Arbitrary React element. - PropTypes.node, - ]).isRequired, - title: PropTypes.string, - className: PropTypes.string, -}; - Icon.defaultProps = { title: null, className: null, diff --git a/lib/components/Icon.test.js b/lib/components/Icon.test.js index 38bbbb9a..be7a298e 100644 --- a/lib/components/Icon.test.js +++ b/lib/components/Icon.test.js @@ -1,18 +1,14 @@ -import PropTypes from "prop-types"; import React from "react"; import { shallow } from "enzyme"; import Icon from "./Icon"; const SQUARE = "M10 10 H 90 V 90 H 10 Z"; +// eslint-disable-next-line @thibaudcolas/cookbook/react/prop-types const CustomIcon = ({ icon }) => ( ); -CustomIcon.propTypes = { - icon: PropTypes.string.isRequired, -}; - describe("Icon", () => { it("#className", () => { expect( diff --git a/lib/components/Toolbar.js b/lib/components/Toolbar.js index 64ad0895..88273c05 100644 --- a/lib/components/Toolbar.js +++ b/lib/components/Toolbar.js @@ -1,10 +1,24 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; +import type { ComponentType } from "react"; +import { EditorState } from "draft-js"; import ToolbarDefaults from "./ToolbarDefaults"; +import type { ToolbarDefaultProps } from "./ToolbarDefaults"; import ToolbarGroup from "./ToolbarGroup"; -const Toolbar = (props) => { +type ControlProps = {| + getEditorState: () => EditorState, + onChange: (EditorState) => void, +|}; + +type Props = { + controls: $ReadOnlyArray>, + getEditorState: () => EditorState, + onChange: (EditorState) => void, +} & ToolbarDefaultProps; + +const Toolbar = (props: Props) => { const { controls, getEditorState, onChange } = props; return (
@@ -24,10 +38,4 @@ const Toolbar = (props) => { ); }; -Toolbar.propTypes = { - controls: PropTypes.array.isRequired, - getEditorState: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, -}; - export default Toolbar; diff --git a/lib/components/ToolbarButton.js b/lib/components/ToolbarButton.js index 0a332a6e..6ec86c9a 100644 --- a/lib/components/ToolbarButton.js +++ b/lib/components/ToolbarButton.js @@ -1,14 +1,30 @@ -import PropTypes from "prop-types"; +// @flow import React, { PureComponent } from "react"; import Icon from "./Icon"; +import type { IconProp } from "./Icon"; + +type Props = {| + name: ?string, + active: boolean, + label: ?string, + title: ?string, + icon: ?IconProp, + onClick: ?(string) => void, +|}; + +type State = {| + showTooltipOnHover: boolean, +|}; /** * Displays a basic button, with optional active variant, * enriched with a tooltip. The tooltip stops showing on click. */ -class ToolbarButton extends PureComponent { - constructor(props) { +class ToolbarButton extends PureComponent { + static defaultProps: Props; + + constructor(props: Props) { super(props); this.state = { @@ -19,7 +35,8 @@ class ToolbarButton extends PureComponent { this.onMouseLeave = this.onMouseLeave.bind(this); } - onMouseDown(e) { + /* :: onMouseDown: (e: Event) => void; */ + onMouseDown(e: Event) { const { name, onClick } = this.props; e.preventDefault(); @@ -28,9 +45,12 @@ class ToolbarButton extends PureComponent { showTooltipOnHover: false, }); - onClick(name); + if (onClick) { + onClick(name || ""); + } } + /* :: onMouseLeave: () => void; */ onMouseLeave() { this.setState({ showTooltipOnHover: true, @@ -54,7 +74,9 @@ class ToolbarButton extends PureComponent { onMouseDown={this.onMouseDown} onMouseLeave={this.onMouseLeave} > - {icon ? : null} + {typeof icon !== "undefined" && icon !== null ? ( + + ) : null} {label ? ( {label} ) : null} @@ -63,26 +85,13 @@ class ToolbarButton extends PureComponent { } } -ToolbarButton.propTypes = { - name: PropTypes.string, - active: PropTypes.bool, - label: PropTypes.string, - title: PropTypes.string, - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - onClick: PropTypes.func, -}; - ToolbarButton.defaultProps = { name: null, active: false, label: null, title: null, icon: null, - onClick: () => {}, + onClick: null, }; export default ToolbarButton; diff --git a/lib/components/ToolbarDefaults.js b/lib/components/ToolbarDefaults.js index 71c8cf1f..206e8850 100644 --- a/lib/components/ToolbarDefaults.js +++ b/lib/components/ToolbarDefaults.js @@ -1,4 +1,4 @@ -import PropTypes from "prop-types"; +// @flow import React, { PureComponent } from "react"; import ToolbarButton from "./ToolbarButton"; @@ -13,11 +13,36 @@ import { DESCRIPTIONS, } from "../api/constants"; import behavior from "../api/behavior"; +import type { IconProp } from "./Icon"; -const getButtonLabel = (type, icon, label = icon ? null : LABELS[type]) => - label; +type ControlProp = { + // Describes the control in the editor UI, concisely. + label?: string, + // Describes the control in the editor UI. + description?: string, + // Represents the control in the editor UI. + icon?: IconProp, +}; + +const getButtonLabel = (type: string, config: boolean | ControlProp) => { + const icon = typeof config === "boolean" ? undefined : config.icon; -const getButtonTitle = (type, description = DESCRIPTIONS[type]) => { + if (typeof config.label === "string") { + return config.label; + } + + if (typeof icon !== "undefined") { + return null; + } + + return LABELS[type]; +}; + +const getButtonTitle = (type: string, config: boolean | ControlProp) => { + const description = + typeof config === "boolean" || typeof config.description === "undefined" + ? DESCRIPTIONS[type] + : config.description; const hasShortcut = behavior.hasKeyboardShortcut(type); let title = description; @@ -29,7 +54,36 @@ const getButtonTitle = (type, description = DESCRIPTIONS[type]) => { return title; }; -class ToolbarDefaults extends PureComponent { +export type ToolbarDefaultProps = { + currentStyles: { + has: (style: string) => boolean, + }, + currentBlock: string, + enableHorizontalRule: boolean | ControlProp, + enableLineBreak: boolean | ControlProp, + showUndoControl: boolean | ControlProp, + showRedoControl: boolean | ControlProp, + entityTypes: $ReadOnlyArray<{ + ...ControlProp, + type: string, + }>, + blockTypes: $ReadOnlyArray<{ + ...ControlProp, + type: string, + }>, + inlineStyles: $ReadOnlyArray<{ + ...ControlProp, + type: string, + }>, + toggleBlockType: (blockType: string) => void, + toggleInlineStyle: (inlineStyle: string) => void, + addHR: () => void, + addBR: () => void, + onUndoRedo: (type: string) => void, + onRequestSource: (entityType: string) => void, +}; + +class ToolbarDefaults extends PureComponent { render() { const { currentStyles, @@ -55,8 +109,8 @@ class ToolbarDefaults extends PureComponent { key={t.type} name={t.type} active={currentStyles.has(t.type)} - label={getButtonLabel(t.type, t.icon, t.label)} - title={getButtonTitle(t.type, t.description)} + label={getButtonLabel(t.type, t)} + title={getButtonTitle(t.type, t)} icon={t.icon} onClick={toggleInlineStyle} /> @@ -69,8 +123,8 @@ class ToolbarDefaults extends PureComponent { key={t.type} name={t.type} active={currentBlock === t.type} - label={getButtonLabel(t.type, t.icon, t.label)} - title={getButtonTitle(t.type, t.description)} + label={getButtonLabel(t.type, t)} + title={getButtonTitle(t.type, t)} icon={t.icon} onClick={toggleBlockType} /> @@ -84,14 +138,17 @@ class ToolbarDefaults extends PureComponent { onClick={addHR} label={getButtonLabel( ENTITY_TYPE.HORIZONTAL_RULE, - enableHorizontalRule.icon, - enableHorizontalRule.label, + enableHorizontalRule, )} title={getButtonTitle( ENTITY_TYPE.HORIZONTAL_RULE, - enableHorizontalRule.description, + enableHorizontalRule, )} - icon={enableHorizontalRule.icon} + icon={ + typeof enableHorizontalRule !== "boolean" + ? enableHorizontalRule.icon + : null + } /> ) : null} @@ -99,13 +156,11 @@ class ToolbarDefaults extends PureComponent { ) : null} , @@ -116,8 +171,8 @@ class ToolbarDefaults extends PureComponent { key={t.type} name={t.type} onClick={onRequestSource} - label={getButtonLabel(t.type, t.icon, t.label)} - title={getButtonTitle(t.type, t.description)} + label={getButtonLabel(t.type, t)} + title={getButtonTitle(t.type, t)} icon={t.icon} /> ))} @@ -128,12 +183,8 @@ class ToolbarDefaults extends PureComponent { ) : null} @@ -141,12 +192,8 @@ class ToolbarDefaults extends PureComponent { ) : null} , @@ -154,78 +201,4 @@ class ToolbarDefaults extends PureComponent { } } -ToolbarDefaults.propTypes = { - currentStyles: PropTypes.object.isRequired, - currentBlock: PropTypes.string.isRequired, - enableHorizontalRule: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]).isRequired, - enableLineBreak: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]).isRequired, - showUndoControl: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]).isRequired, - showRedoControl: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.shape({ - // Describes the control in the editor UI, concisely. - label: PropTypes.string, - // Describes the control in the editor UI. - description: PropTypes.string, - // Represents the control in the editor UI. - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - PropTypes.node, - ]), - }), - ]).isRequired, - entityTypes: PropTypes.array.isRequired, - blockTypes: PropTypes.array.isRequired, - inlineStyles: PropTypes.array.isRequired, - toggleBlockType: PropTypes.func.isRequired, - toggleInlineStyle: PropTypes.func.isRequired, - addHR: PropTypes.func.isRequired, - addBR: PropTypes.func.isRequired, - onUndoRedo: PropTypes.func.isRequired, - onRequestSource: PropTypes.func.isRequired, -}; - export default ToolbarDefaults; diff --git a/lib/components/ToolbarGroup.js b/lib/components/ToolbarGroup.js index 581b1728..34b47823 100644 --- a/lib/components/ToolbarGroup.js +++ b/lib/components/ToolbarGroup.js @@ -1,17 +1,18 @@ -import PropTypes from "prop-types"; +// @flow import React from "react"; +import type { Node } from "react"; -const ToolbarGroup = ({ children }) => { +type Props = {| + children?: Node, +|}; + +const ToolbarGroup = ({ children }: Props) => { const hasChildren = React.Children.toArray(children).some((c) => c !== null); return hasChildren ? (
{children}
) : null; }; -ToolbarGroup.propTypes = { - children: PropTypes.node, -}; - ToolbarGroup.defaultProps = { children: null, }; diff --git a/lib/components/__snapshots__/DraftailEditor.test.js.snap b/lib/components/__snapshots__/DraftailEditor.test.js.snap index 872f44db..564e01bf 100644 --- a/lib/components/__snapshots__/DraftailEditor.test.js.snap +++ b/lib/components/__snapshots__/DraftailEditor.test.js.snap @@ -188,7 +188,6 @@ exports[`DraftailEditor #maxListNesting 1`] = ` />
`; @@ -381,7 +380,6 @@ exports[`DraftailEditor empty 1`] = ` />
`; diff --git a/lib/index.js b/lib/index.js index 58a8fc32..14844b92 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,4 @@ +// @flow /** * Draftail's main API entry point. Exposes all of the modules people * will need to create their own editor instances from Draftail. diff --git a/lib/utils/getComponentWrapper.js b/lib/utils/getComponentWrapper.js index 3726c20a..6651981a 100644 --- a/lib/utils/getComponentWrapper.js +++ b/lib/utils/getComponentWrapper.js @@ -1,10 +1,15 @@ +// @flow import React from "react"; +import type { ComponentType } from "react"; /** * Wraps a component to provide it with additional props based on context. */ -const getComponentWrapper = (Wrapped, wrapperProps) => { - const Wrapper = (props) => ; +const getComponentWrapper = (Wrapped: ComponentType<{}>, wrapperProps: {}) => { + const Wrapper = (props: {}) => ( + // flowlint inexact-spread:off + + ); return Wrapper; }; diff --git a/package-lock.json b/package-lock.json index 916f479e..19fbf519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -414,9 +414,9 @@ } }, "@babel/plugin-syntax-flow": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.0.0.tgz", - "integrity": "sha512-zGcuZWiWWDa5qTZ6iAnpG0fnX/GOu49pGR5PFvkQ9GmKNaSphXQnlNXh/LG20sqWtNrx/eB6krzfEzcwvUyeFA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.2.0.tgz", + "integrity": "sha512-r6YMuZDWLtLlu0kqIim5o/3TNRAlWb073HwT3e2nKf9I8IIvOggPrnILYPsrrKilmn/mYEMCf/Z07w3yQJF6dg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" @@ -570,13 +570,13 @@ } }, "@babel/plugin-transform-flow-strip-types": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.0.0.tgz", - "integrity": "sha512-WhXUNb4It5a19RsgKKbQPrjmy4yWOY1KynpEbNw7bnd1QTcrT/EIl3MJvnGgpgvrKyKbqX7nUNOJfkpLOnoDKA==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.2.3.tgz", + "integrity": "sha512-xnt7UIk9GYZRitqCnsVMjQK1O2eKZwFB3CvvHjf5SGx6K6vr/MScCKQDnf1DxRaj501e3pXjti+inbSXX2ZUoQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0" + "@babel/plugin-syntax-flow": "^7.2.0" } }, "@babel/plugin-transform-for-of": { @@ -3046,6 +3046,58 @@ "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", "dev": true }, + "babel-eslint": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz", + "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + } + } + }, "babel-generator": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", @@ -3354,12 +3406,6 @@ "esutils": "^2.0.2" } }, - "babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.18.tgz", - "integrity": "sha512-azed2nHo8vmOy7EY26KH+om5oOcWRs0r1U8wOmhwta+SBMMnmJ4H6yaBZRCcHBtMeWp9AVhvBTL/lpR1kEx+Xw==", - "dev": true - }, "babel-plugin-transform-regexp-constructors": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.4.3.tgz", @@ -6072,14 +6118,14 @@ } }, "draftjs-conductor": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-0.2.1.tgz", - "integrity": "sha512-oazG/8otKjTZ1OdAA0BDaYiRsi4tJd0UmNjtIFebaqWUryyr76JyA2j/trFejEHQuNnSHjjeci4s9Jn4sxXPEA==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-0.4.1.tgz", + "integrity": "sha512-5BcJLdYLNIA/TNp/9xwIeD1quWsWEoi0ZI81TiW3vLecBEQgWKVxuKZaLdaXHYpW4/kdQvAJz2KcOBTpJkYBxg==" }, "draftjs-filters": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/draftjs-filters/-/draftjs-filters-2.2.1.tgz", - "integrity": "sha512-O1vWQyf0p599131JcMFF5uN5zM3lxf9tj5fIUqkJO7C4aBFS0GDuRoO68tJigtfhdhwhLjwwrsQfQdjTd17hwA==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/draftjs-filters/-/draftjs-filters-2.2.3.tgz", + "integrity": "sha512-xbpXgjTtFzaMp9P9xlaqmf3NpP4yYM3OT3HbodUQpI4t2kO9ltJTo0+3H+1PlsUPKoc7jPthiE0MYN/VMrsuNw==" }, "duplexer": { "version": "0.1.1", @@ -6771,6 +6817,15 @@ } } }, + "eslint-plugin-flowtype": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.2.0.tgz", + "integrity": "sha512-baJmzngM6UKbEkJ5OY3aGw2zjXBt5L2QKZvTsOlXX7yHKIjNRrlJx2ods8Rng6EdqPR9rVNIQNYHpTs0qfn2qA==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, "eslint-plugin-import": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz", @@ -7894,6 +7949,12 @@ "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", "dev": true }, + "flow-bin": { + "version": "0.92.1", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.92.1.tgz", + "integrity": "sha512-F5kC5oQOR2FXROAeybJHFqgZP+moKV9fa/53QK4Q4WayTQHdA0KSl48KD1gP0A9mioRLiKUegTva/7I15cX3Iw==", + "dev": true + }, "flush-write-stream": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", @@ -13998,9 +14059,9 @@ "dev": true }, "prettier": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.14.0.tgz", - "integrity": "sha512-KtQ2EGaUwf2EyDfp1fxyEb0PqGKakVm0WyXwDt6u+cAoxbO2Z2CwKvOe3+b4+F2IlO9lYHi1kqFuRM70ddBnow==", + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz", + "integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==", "dev": true }, "pretty-error": { diff --git a/package.json b/package.json index 77266366..17840d83 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,13 @@ "not OperaMini all" ], "dependencies": { - "draftjs-conductor": "^0.2.1", - "draftjs-filters": "^2.2.1" + "draftjs-conductor": "^0.4.1", + "draftjs-filters": "^2.2.3" }, "devDependencies": { "@babel/core": "^7.1.2", "@babel/preset-env": "^7.1.0", + "@babel/preset-flow": "^7.0.0", "@babel/preset-react": "^7.0.0", "@sentry/browser": "^4.2.3", "@storybook/addon-viewport": "^4.0.4", @@ -52,9 +53,9 @@ "@thibaudcolas/stylelint-config-cookbook": "^2.0.1", "autoprefixer": "^7.1.2", "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^10.0.1", "babel-jest": "^23.6.0", "babel-loader": "^8.0.4", - "babel-plugin-transform-react-remove-prop-types": "^0.4.18", "core-js": "^2.5.1", "danger": "^4.0.2", "dotenv": "^6.0.0", @@ -65,7 +66,9 @@ "enzyme-to-json": "^3.3.4", "eslint": "^5.6.0", "eslint-plugin-compat": "^2.5.1", + "eslint-plugin-flowtype": "^3.2.0", "express": "^4.16.3", + "flow-bin": "^0.92.1", "formik": "^1.4.1", "immutable": "~3.7.4", "jest": "^23.6.0", @@ -77,9 +80,8 @@ "normalize.css": "^7.0.0", "postcss-cli": "^6.0.1", "postcss-loader": "^3.0.0", - "prettier": "^1.14.0", + "prettier": "^1.16.4", "prismjs": "^1.8.4", - "prop-types": "^15.6.0", "puppeteer": "^1.11.0", "react": "^16.6.0", "react-benchmark": "^2.1.0", @@ -102,7 +104,6 @@ }, "peerDependencies": { "draft-js": "^0.10.5", - "prop-types": "^15.5.0", "react": "^16.0.0", "react-dom": "^16.0.0" }, @@ -114,8 +115,8 @@ "build": "npm run build:rollup -s && npm run build:storybook -s && npm run build:styles -s", "dist": "NODE_ENV=production npm run build -s", "danger": "danger ci --verbose", - "lint": "eslint . && stylelint '**/*.scss' && prettier --list-different '**/*.{js,scss,css,json,md,yml,yaml}'", - "format": "prettier --write '**/*.{js,scss,css,json,md,yml,yaml}'", + "lint": "eslint . && flow && stylelint '**/*.scss' && prettier --check '**/*.{js,scss,css,json,md,yml,yaml,html}'", + "format": "prettier --write '**/*.{js,scss,css,json,md,yml,yaml,html}'", "test": "jest", "test:integration": "jest --config tests/integration/jest.config.js", "test:integration:watch": "jest --config tests/integration/jest.config.js --watch", diff --git a/public/examples/index.html b/public/examples/index.html index b833eee7..dd106906 100644 --- a/public/examples/index.html +++ b/public/examples/index.html @@ -1,11 +1,18 @@ - + Redirecting… - - - + + +

Redirecting…

- Click here if you are not redirected. - + Click here if you are not redirected. + diff --git a/public/index.html b/public/index.html index 36ae5c14..d7097615 100644 --- a/public/index.html +++ b/public/index.html @@ -1,11 +1,13 @@ - + Redirecting… - - - + + +

Redirecting…

Click here if you are not redirected. - + diff --git a/tests/performance/markov_draftjs_41.test.js b/tests/performance/markov_draftjs_41.test.js index 676d236f..4fb9e57b 100644 --- a/tests/performance/markov_draftjs_41.test.js +++ b/tests/performance/markov_draftjs_41.test.js @@ -29,7 +29,7 @@ describe("performance", () => { />, ); component.instance().start(); - expect(results.mean).toBeLessThan(77 * PERFORMANCE_BUFFER); + expect(results.mean).toBeLessThan(87 * PERFORMANCE_BUFFER); expect(results.min).toBeLessThan(49 * PERFORMANCE_BUFFER); expect(results.median).toBeLessThan(61 * PERFORMANCE_BUFFER); expect(results.max).toBeLessThan(278 * PERFORMANCE_BUFFER);