diff --git a/.changeset/brown-eagles-shout.md b/.changeset/brown-eagles-shout.md new file mode 100644 index 0000000000..dbdbf29453 --- /dev/null +++ b/.changeset/brown-eagles-shout.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": patch +"@khanacademy/perseus-editor": patch +--- + +Removing final usage of createReactClass. diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index 7a1d2f0618..deb2c3b791 100644 --- a/packages/perseus-editor/package.json +++ b/packages/perseus-editor/package.json @@ -63,7 +63,6 @@ "@phosphor-icons/core": "^2.0.2", "aphrodite": "^1.2.5", "classnames": "1.1.4", - "create-react-class": "15.6.3", "jquery": "^2.1.1", "katex": "0.11.1", "perseus-build-settings": "^0.4.1", @@ -91,7 +90,6 @@ "@phosphor-icons/core": "^2.0.2", "aphrodite": "^1.2.5", "classnames": "1.1.4", - "create-react-class": "15.6.3", "jquery": "^2.1.1", "prop-types": "15.6.1", "react": "^18.2.0", @@ -99,4 +97,4 @@ "underscore": "^1.4.4" }, "keywords": [] -} +} \ No newline at end of file diff --git a/packages/perseus-editor/src/components/graph-settings.tsx b/packages/perseus-editor/src/components/graph-settings.tsx index 27bc39778b..eaeda8e2ff 100644 --- a/packages/perseus-editor/src/components/graph-settings.tsx +++ b/packages/perseus-editor/src/components/graph-settings.tsx @@ -11,12 +11,12 @@ import { Util, } from "@khanacademy/perseus"; import {Checkbox} from "@khanacademy/wonder-blocks-form"; -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; import * as React from "react"; import ReactDOM from "react-dom"; import _ from "underscore"; +import type {Coords} from "@khanacademy/perseus"; + const {ButtonGroup, InfoTip, RangeInput} = components; const defaultBackgroundImage = { @@ -28,70 +28,111 @@ const defaultBackgroundImage = { function numSteps(range: any, step: any) { return Math.floor((range[1] - range[0]) / step); } - -const GraphSettings = createReactClass({ - displayName: "GraphSettings", - - propTypes: { - ...Changeable.propTypes, - editableSettings: PropTypes.arrayOf( - PropTypes.oneOf(["canvas", "graph", "snap", "image", "measure"]), - ), - box: PropTypes.arrayOf(PropTypes.number), - labels: PropTypes.arrayOf(PropTypes.string), - range: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), - step: PropTypes.arrayOf(PropTypes.number), - gridStep: PropTypes.arrayOf(PropTypes.number), - snapStep: PropTypes.arrayOf(PropTypes.number), - valid: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - backgroundImage: PropTypes.object, - markings: PropTypes.oneOf(["graph", "grid", "none"]), - showProtractor: PropTypes.bool, - showRuler: PropTypes.bool, - showTooltips: PropTypes.bool, - rulerLabel: PropTypes.string, - rulerTicks: PropTypes.number, - }, - - getDefaultProps: function () { - return { - editableSettings: ["graph", "snap", "image", "measure"], - box: [ - interactiveSizes.defaultBoxSizeSmall, - interactiveSizes.defaultBoxSizeSmall, - ], - labels: ["x", "y"], - range: [ - [-10, 10], - [-10, 10], - ], - step: [1, 1], - gridStep: [1, 1], - snapStep: [1, 1], - valid: true, - backgroundImage: defaultBackgroundImage, - markings: "graph", - showProtractor: false, - showRuler: false, - showTooltips: false, - rulerLabel: "", - rulerTicks: 10, - }; - }, - - getInitialState: function () { +type Props = { + editableSettings: ReadonlyArray< + "canvas" | "graph" | "snap" | "image" | "measure" + >; + box: readonly number[]; + labels: readonly string[]; + range: Coords; + step: [number, number]; + gridStep: [number, number]; + snapStep: [number, number]; + valid: boolean; + backgroundImage: any; + markings: "graph" | "grid" | "none"; + showProtractor?: boolean; + showRuler?: boolean; + showTooltips?: boolean; + rulerLabel: string; + rulerTicks: number; +} & Changeable.ChangeableProps; + +type DefaultProps = { + editableSettings: Props["editableSettings"]; + box: Props["box"]; + labels: Props["labels"]; + range: Props["range"]; + step: Props["step"]; + gridStep: Props["gridStep"]; + snapStep: Props["snapStep"]; + valid: Props["valid"]; + backgroundImage: Props["backgroundImage"]; + markings: Props["markings"]; + rulerLabel: Props["rulerLabel"]; + rulerTicks: Props["rulerTicks"]; + showProtractor?: Props["showProtractor"]; + showRuler?: Props["showRuler"]; + showTooltips?: Props["showTooltips"]; +}; + +type State = { + labelsTextbox: string[]; + gridStepTextbox: number[]; + snapStepTextbox: number[]; + stepTextbox: number[]; + rangeTextbox: any[]; + backgroundImage: any; +}; + +class GraphSettings extends React.Component { + static displayName: "GraphSettings"; + + static defaultProps: DefaultProps = { + editableSettings: ["graph", "snap", "image", "measure"], + box: [ + interactiveSizes.defaultBoxSizeSmall, + interactiveSizes.defaultBoxSizeSmall, + ], + labels: ["x", "y"], + range: [ + [-10, 10], + [-10, 10], + ], + step: [1, 1], + gridStep: [1, 1], + snapStep: [1, 1], + valid: true, + backgroundImage: defaultBackgroundImage, + markings: "graph", + rulerLabel: "", + rulerTicks: 10, + showProtractor: false, + showRuler: false, + showTooltips: false, + }; + + _isMounted = false; + + constructor(props) { + super(props); + this.state = this.getInitialState(); + + this.change = this.change.bind(this); + this.changeBackgroundUrl = this.changeBackgroundUrl.bind(this); + this.changeGraph = this.changeGraph.bind(this); + this.changeGridStep = this.changeGridStep.bind(this); + this.changeLabel = this.changeLabel.bind(this); + this.changeRange = this.changeRange.bind(this); + this.changeRulerLabel = this.changeRulerLabel.bind(this); + this.changeRulerTicks = this.changeRulerTicks.bind(this); + this.changeSnapStep = this.changeSnapStep.bind(this); + this.changeStep = this.changeStep.bind(this); + } + + getInitialState() { return this.stateFromProps(this.props); - }, + } - componentDidMount: function () { + componentDidMount() { // TODO(scottgrant): This is a hack to remove the deprecated call to // this.isMounted() but is still considered an anti-pattern. this._isMounted = true; this.changeGraph = _.debounce(this.changeGraph, 300); - }, + } - UNSAFE_componentWillReceiveProps: function (nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if ( !_.isEqual(this.props.labels, nextProps.labels) || !_.isEqual(this.props.gridStep, nextProps.gridStep) || @@ -102,13 +143,13 @@ const GraphSettings = createReactClass({ ) { this.setState(this.stateFromProps(nextProps)); } - }, + } - componentWillUnmount: function () { + componentWillUnmount() { this._isMounted = false; - }, + } - stateFromProps: function (props) { + stateFromProps(props) { return { labelsTextbox: props.labels, gridStepTextbox: props.gridStep, @@ -117,7 +158,7 @@ const GraphSettings = createReactClass({ rangeTextbox: props.range, backgroundImage: _.clone(props.backgroundImage), }; - }, + } // TODO(benchristel): Refactor this component to be an ES6 class, so we can // type change as ChangeFn. @@ -125,19 +166,19 @@ const GraphSettings = createReactClass({ // TODO(LEMS-2656): remove TS suppression // @ts-expect-error: Argument of type 'any[]' is not assignable to parameter of type '[newPropsOrSinglePropName: string | { [key: string]: any; }, propValue?: any, callback?: (() => unknown) | undefined]'. Target requires 1 element(s) but source may have fewer. return Changeable.change.apply(this, args); - }, + } // TODO(aria): Make either a wrapper for standard events to work // with this.change, or make these use some TextInput/NumberInput box - changeRulerLabel: function (e) { + changeRulerLabel(e) { this.change({rulerLabel: e.target.value}); - }, + } - changeRulerTicks: function (e) { + changeRulerTicks(e) { this.change({rulerTicks: +e.target.value}); - }, + } - changeBackgroundUrl: function (e) { + changeBackgroundUrl(e) { // Only continue on blur or "enter" if (e.type === "keypress" && e.key !== "Enter") { return; @@ -167,9 +208,9 @@ const GraphSettings = createReactClass({ } else { setUrl(null, 0, 0); } - }, + } - renderLabelChoices: function (choices) { + renderLabelChoices(choices) { return _.map(choices, function ([name, value]) { return ( ); }); - }, + } - validRange: function (range) { + validRange(range) { const numbers = _.every(range, function (num) { return _.isFinite(num); }); @@ -190,9 +231,9 @@ const GraphSettings = createReactClass({ return "Range must have a higher number on the right"; } return true; - }, + } - validateStepValue: function (settings) { + validateStepValue(settings) { const {step, range, name, minTicks, maxTicks} = settings; if (!_.isFinite(step)) { @@ -216,9 +257,9 @@ const GraphSettings = createReactClass({ ); } return true; - }, + } - validSnapStep: function (step, range) { + validSnapStep(step, range) { return this.validateStepValue({ step: step, range: range, @@ -226,9 +267,9 @@ const GraphSettings = createReactClass({ minTicks: 5, maxTicks: 60, }); - }, + } - validGridStep: function (step, range) { + validGridStep(step, range) { return this.validateStepValue({ step: step, range: range, @@ -236,9 +277,9 @@ const GraphSettings = createReactClass({ minTicks: 3, maxTicks: 60, }); - }, + } - validStep: function (step, range) { + validStep(step, range) { return this.validateStepValue({ step: step, range: range, @@ -246,9 +287,9 @@ const GraphSettings = createReactClass({ minTicks: 3, maxTicks: 20, }); - }, + } - validBackgroundImageSize: function (image) { + validBackgroundImageSize(image) { // Ignore empty images if (!image.url) { return true; @@ -260,9 +301,9 @@ const GraphSettings = createReactClass({ return "Image must be smaller than 450px x 450px."; } return true; - }, + } - validateGraphSettings: function (range, step, gridStep, snapStep, image) { + validateGraphSettings(range, step, gridStep, snapStep, image) { const self = this; let msg; const goodRange = _.every(range, function (range) { @@ -299,16 +340,16 @@ const GraphSettings = createReactClass({ return msg; } return true; - }, + } - changeLabel: function (i, e) { + changeLabel(i, e) { const val = e.target.value; const labels = this.state.labelsTextbox.slice(); labels[i] = val; this.setState({labelsTextbox: labels}, this.changeGraph); - }, + } - changeRange: function (i, values) { + changeRange(i, values) { const ranges = this.state.rangeTextbox.slice(); ranges[i] = values; const step = this.state.stepTextbox.slice(); @@ -317,7 +358,10 @@ const GraphSettings = createReactClass({ const scale = Util.scaleFromExtent(ranges[i], this.props.box[i]); if (this.validRange(ranges[i]) === true) { step[i] = Util.tickStepFromExtent(ranges[i], this.props.box[i]); - gridStep[i] = Util.gridStepFromTickStep(step[i], scale); + // Need to use the cast to number since gridStepFromTickStep + // can return null or undefined. Ideally in the future let's adjust + // the code to take this into account. + gridStep[i] = Util.gridStepFromTickStep(step[i], scale) as number; snapStep[i] = gridStep[i] / 2; } this.setState( @@ -329,17 +373,17 @@ const GraphSettings = createReactClass({ }, this.changeGraph, ); - }, + } - changeStep: function (step) { + changeStep(step) { this.setState({stepTextbox: step}, this.changeGraph); - }, + } - changeSnapStep: function (snapStep) { + changeSnapStep(snapStep) { this.setState({snapStepTextbox: snapStep}, this.changeGraph); - }, + } - changeGridStep: function (gridStep) { + changeGridStep(gridStep) { this.setState( { gridStepTextbox: gridStep, @@ -349,9 +393,9 @@ const GraphSettings = createReactClass({ }, this.changeGraph, ); - }, + } - changeGraph: function () { + changeGraph() { const labels = this.state.labelsTextbox; const range = _.map(this.state.rangeTextbox, function (range) { return _.map(range, Number); @@ -390,9 +434,9 @@ const GraphSettings = createReactClass({ valid: validationResult, // a string message, not false }); } - }, + } - render: function () { + render() { const scale = [ KhanMath.roundTo( 2, @@ -649,7 +693,7 @@ const GraphSettings = createReactClass({ )} ); - }, -}); + } +} export default GraphSettings; diff --git a/packages/perseus-editor/src/components/json-editor.tsx b/packages/perseus-editor/src/components/json-editor.tsx index 1b2a0576fc..bce9343163 100644 --- a/packages/perseus-editor/src/components/json-editor.tsx +++ b/packages/perseus-editor/src/components/json-editor.tsx @@ -1,30 +1,62 @@ /* eslint-disable @babel/no-invalid-this */ /* eslint-disable react/no-unsafe */ -import createReactClass from "create-react-class"; import * as React from "react"; import _ from "underscore"; -const JsonEditor: any = createReactClass({ - displayName: "JsonEditor", +type Props = { + multiLine: boolean; + value: any; + onChange: (newJson: any) => void; +}; - getInitialState: function () { +type DefaultProps = { + value: Props["value"]; +}; + +type State = { + currentValue: string | undefined; + valid: boolean | undefined; +}; + +class JsonEditor extends React.Component { + static displayName: "JsonEditor"; + + static defaultProps: DefaultProps = { + value: {}, + }; + + constructor(props) { + super(props); + this.state = this.getInitialState(); + + this.handleBlur = this.handleBlur.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + getInitialState() { return { currentValue: JSON.stringify(this.props.value, null, 4), valid: true, }; - }, + } - UNSAFE_componentWillReceiveProps: function (nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const shouldReplaceContent = !this.state.valid || - !_.isEqual(nextProps.value, JSON.parse(this.state.currentValue)); + !_.isEqual( + nextProps.value, + JSON.parse( + this.state.currentValue ? this.state.currentValue : "", + ), + ); if (shouldReplaceContent) { this.setState(this.getInitialState()); } - }, + } - handleKeyDown: function (e) { + handleKeyDown(e) { // This handler allows the tab character to be entered by pressing // tab, instead of jumping to the next (non-existant) field if (e.key === "Tab") { @@ -39,9 +71,9 @@ const JsonEditor: any = createReactClass({ e.preventDefault(); this.handleChange(e); } - }, + } - handleChange: function (e) { + handleChange(e) { const nextString = e.target.value; try { let json = JSON.parse(nextString); @@ -68,11 +100,11 @@ const JsonEditor: any = createReactClass({ valid: false, }); } - }, + } // You can type whatever you want as you're typing, but if it's not valid // when you blur, it will revert to the last valid value. - handleBlur: function (e) { + handleBlur(e) { const nextString = e.target.value; try { let json = JSON.parse(nextString); @@ -99,9 +131,9 @@ const JsonEditor: any = createReactClass({ valid: true, }); } - }, + } - render: function () { + render() { const classes = "perseus-json-editor " + (this.state.valid ? "valid" : "invalid"); @@ -114,7 +146,7 @@ const JsonEditor: any = createReactClass({ onBlur={this.handleBlur} /> ); - }, -}); + } +} export default JsonEditor; diff --git a/packages/perseus-editor/src/widgets/interaction-editor/interaction-editor.tsx b/packages/perseus-editor/src/widgets/interaction-editor/interaction-editor.tsx index 7ca89308b5..249dae9ad5 100644 --- a/packages/perseus-editor/src/widgets/interaction-editor/interaction-editor.tsx +++ b/packages/perseus-editor/src/widgets/interaction-editor/interaction-editor.tsx @@ -21,31 +21,18 @@ import ParametricEditor from "./parametric-editor"; import PointEditor from "./point-editor"; import RectangleEditor from "./rectangle-editor"; +import type {Coords} from "@khanacademy/perseus"; + const {getDependencies} = Dependencies; const {unescapeMathMode} = Util; -const defaultInteractionProps = { - graph: { - box: [400, 400], - labels: ["x", "y"], - range: [ - [-10, 10], - [-10, 10], - ], - tickStep: [1, 1], - gridStep: [1, 1], - markings: "graph", - }, - elements: [], -} as const; - type Graph = { box: ReadonlyArray; labels: ReadonlyArray; - range: ReadonlyArray>; - tickStep: ReadonlyArray; - gridStep: ReadonlyArray; - markings: string; + range: Coords; + tickStep: [number, number]; + gridStep: [number, number]; + markings: "graph" | "grid" | "none"; valid?: boolean; }; @@ -63,7 +50,20 @@ type State = any; class InteractionEditor extends React.Component { static widgetName = "interaction" as const; - static defaultProps: DefaultProps = defaultInteractionProps; + static defaultProps: DefaultProps = { + graph: { + box: [400, 400], + labels: ["x", "y"], + range: [ + [-10, 10], + [-10, 10], + ], + tickStep: [1, 1], + gridStep: [1, 1], + markings: "graph", + }, + elements: [], + }; state: State = { usedVarSubscripts: this._getAllVarSubscripts(this.props.elements), diff --git a/packages/perseus/package.json b/packages/perseus/package.json index 4d7fa99c29..1e8abd4053 100644 --- a/packages/perseus/package.json +++ b/packages/perseus/package.json @@ -77,7 +77,6 @@ "@popperjs/core": "^2.10.2", "aphrodite": "^1.2.5", "classnames": "1.1.4", - "create-react-class": "15.6.3", "csstype": "^3.1.3", "intersection-observer": "^0.12.0", "jquery": "^2.1.1", @@ -113,7 +112,6 @@ "@popperjs/core": "^2.10.2", "aphrodite": "^1.2.5", "classnames": "1.1.4", - "create-react-class": "15.6.3", "intersection-observer": "^0.12.0", "jquery": "^2.1.1", "lodash.debounce": "^4.0.8", @@ -124,4 +122,4 @@ "underscore": "^1.4.4" }, "keywords": [] -} +} \ No newline at end of file diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index bbcf1a6059..cd31373dee 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -241,6 +241,7 @@ export type { } from "./perseus-types"; export type {UserInputMap} from "./validation.types"; export type {Coord} from "./interactive2/types"; +export type {Coords} from "./widgets/grapher/grapher-types"; export type {MarkerType} from "./widgets/label-image/types"; export type { RendererPromptJSON, diff --git a/yarn.lock b/yarn.lock index 14465d0d62..ba6d96971c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5469,15 +5469,6 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -create-react-class@15.6.3: - version "15.6.3" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" - integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg== - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - cross-env@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d" @@ -7308,7 +7299,7 @@ fbjs-css-vars@^1.0.0: resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== -fbjs@^0.8.16, fbjs@^0.8.9: +fbjs@^0.8.16: version "0.8.18" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.18.tgz#9835e0addb9aca2eff53295cd79ca1cfc7c9662a" integrity sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA==