diff --git a/.changeset/khaki-keys-approve.md b/.changeset/khaki-keys-approve.md new file mode 100644 index 0000000000..57858e31c7 --- /dev/null +++ b/.changeset/khaki-keys-approve.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus-editor": patch +--- + +Removing usage of createReactClass from several component files. diff --git a/packages/perseus-editor/src/components/drag-target.tsx b/packages/perseus-editor/src/components/drag-target.tsx index 4e78e7378f..6319ccbcb4 100644 --- a/packages/perseus-editor/src/components/drag-target.tsx +++ b/packages/perseus-editor/src/components/drag-target.tsx @@ -22,53 +22,75 @@ // * custom styles for global drag and dragOver // * only respond to certain types of drags (only images for instance)! -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; import * as React from "react"; -const DragTarget = createReactClass({ - propTypes: { - // All props not listed here are forwarded to the root element without - // modification. - onDrop: PropTypes.func.isRequired, - component: PropTypes.any, // component type - shouldDragHighlight: PropTypes.func, - style: PropTypes.any, - }, - getDefaultProps: function () { - return { - component: "div", - shouldDragHighlight: () => true, +type Props = { + onDrop: (e: DragEvent) => void; + component?: any; + shouldDragHighlight: (any) => boolean; + style?: any; + children?: any; + className?: string; +}; + +type DefaultProps = { + component: Props["component"]; + shouldDragHighlight: Props["shouldDragHighlight"]; +}; + +type State = { + dragHover: boolean; +}; +class DragTarget extends React.Component { + static defaultProps: DefaultProps = { + component: "div", + shouldDragHighlight: () => true, + }; + + constructor(props) { + super(props); + this.state = { + dragHover: false, }; - }, - getInitialState: function () { - return {dragHover: false}; - }, - handleDrop: function (e) { + + this.handleDrop = this.handleDrop.bind(this); + this.handleDragEnd = this.handleDragEnd.bind(this); + this.handleDragOver = this.handleDragOver.bind(this); + this.handleDragLeave = this.handleDragLeave.bind(this); + this.handleDragEnter = this.handleDragEnter.bind(this); + } + + handleDrop(e: DragEvent) { e.stopPropagation(); e.preventDefault(); this.setState({dragHover: false}); this.props.onDrop(e); - }, - handleDragEnd: function () { + } + + handleDragEnd() { this.setState({dragHover: false}); - }, - handleDragOver: function (e) { + } + + handleDragOver(e) { e.preventDefault(); - }, - handleDragLeave: function () { + } + + handleDragLeave() { this.setState({dragHover: false}); - }, - handleDragEnter: function (e) { + } + + handleDragEnter(e) { this.setState({dragHover: this.props.shouldDragHighlight(e)}); - }, - render: function () { - const opacity = this.state.dragHover ? {opacity: 0.3} : {}; - const Component = this.props.component; + } - const forwardProps = Object.assign({}, this.props); - delete forwardProps.component; - delete forwardProps.shouldDragHighlight; + render() { + const opacity = this.state.dragHover ? {opacity: 0.3} : {}; + const { + component: Component, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + shouldDragHighlight, + ...forwardProps + } = this.props; return ( ); - }, -}); + } +} export default DragTarget; diff --git a/packages/perseus-editor/src/components/sortable.tsx b/packages/perseus-editor/src/components/sortable.tsx index 6ecdcc07d8..a1228350be 100644 --- a/packages/perseus-editor/src/components/sortable.tsx +++ b/packages/perseus-editor/src/components/sortable.tsx @@ -1,59 +1,84 @@ import {css, StyleSheet} from "aphrodite"; -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; import * as React from "react"; import ReactDOM from "react-dom"; -const PT = PropTypes; +type Props = { + className?: string; + components: React.ReactElement[]; + onReorder: (i: any[]) => void; + style?: any; + verify: (i: any) => boolean; +}; + +type DefaultProps = { + verify: Props["verify"]; +}; + +type State = { + dragging: number; + components: React.ReactElement[]; +}; /** + * TODO(LEMS-2667): 11/26/24, at the time of writing this comment + * it has been identified that this file has been broken long before + * the refactoring of createReactClass. Future implementation need + * to determine how to fix this functionality or deprecate it. + * * * Takes an array of components to sort. * As of 08/05/24, there are two sortable components * (one in perseus and one in perseus-editor). * As far as I can tell, this one is only used in ExpressionEditor. */ // eslint-disable-next-line react/no-unsafe -const SortableArea = createReactClass({ - propTypes: { - className: PT.string, - components: PT.arrayOf(PT.node).isRequired, - onReorder: PT.func.isRequired, - style: PT.any, - verify: PT.func, - }, - getDefaultProps: function () { - return {verify: () => true}; - }, - getInitialState: function () { - return { +class SortableArea extends React.Component { + _dragItems: any; + + static defaultProps: DefaultProps = { + verify: () => true, + }; + + constructor(props) { + super(props); + this.state = { // index of the component being dragged - dragging: null, + dragging: -1, components: this.props.components, }; - }, + + this.onDrop = this.onDrop.bind(this); + this.onDragStart = this.onDragStart.bind(this); + this.onDragEnter = this.onDragEnter.bind(this); + } + // Firefox refuses to drag an element unless you set data on it. Hackily // add data each time an item is dragged. - componentDidMount: function () { + componentDidMount() { this._setDragEvents(); - }, + } + // eslint-disable-next-line react/no-unsafe - UNSAFE_componentWillReceiveProps: function (nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { this.setState({components: nextProps.components}); - }, - componentDidUpdate: function () { + } + + componentDidUpdate() { this._setDragEvents(); - }, + } + // Alternatively send each handler to each component individually, // partially applied - onDragStart: function (startIndex) { + onDragStart(startIndex) { this.setState({dragging: startIndex}); - }, - onDrop: function () { + } + + onDrop() { // tell the parent component - this.setState({dragging: null}); + this.setState({dragging: -1}); this.props.onReorder(this.state.components); - }, - onDragEnter: function (enterIndex) { + } + + onDragEnter(enterIndex) { // When a label is first dragged it triggers a dragEnter with itself, // which we don't care about. if (this.state.dragging === enterIndex) { @@ -75,15 +100,18 @@ const SortableArea = createReactClass({ }); } return verified; - }, - _listenEvent: function (e) { + } + + _listenEvent(e) { e.dataTransfer.setData("hackhackhack", "because browsers!"); - }, - _cancelEvent: function (e) { + } + + _cancelEvent(e) { // prevent the browser from redirecting to 'because browsers!' e.preventDefault(); - }, - _setDragEvents: function () { + } + + _setDragEvents() { this._dragItems = this._dragItems || []; const items = // @ts-expect-error - TS2531 - Object is possibly 'null' @@ -117,8 +145,9 @@ const SortableArea = createReactClass({ dragItem.removeEventListener("dragstart", this._listenEvent); dragItem.removeEventListener("drop", this._cancelEvent); } - }, - render: function () { + } + + render() { const sortables = this.state.components.map((component, index) => ( ); - }, -}); + } +} + +type ItemProps = { + area: any; + component: React.ReactNode; + dragging: boolean; + draggable: boolean; + index: number; +}; // An individual sortable item -const SortableItem = createReactClass({ - propTypes: { - area: PT.shape({ - onDragEnter: PT.func.isRequired, - onDragStart: PT.func.isRequired, - onDrop: PT.func.isRequired, - }), - component: PT.node.isRequired, - dragging: PT.bool.isRequired, - draggable: PT.bool.isRequired, - index: PT.number.isRequired, - }, - handleDragStart: function (e) { +class SortableItem extends React.Component { + handleDragStart(e) { e.nativeEvent.dataTransfer.effectAllowed = "move"; this.props.area.onDragStart(this.props.index); - }, - handleDrop: function () { + } + + handleDrop() { this.props.area.onDrop(this.props.index); - }, - handleDragEnter: function (e) { + } + + handleDragEnter(e) { const verified = this.props.area.onDragEnter(this.props.index); // Ideally this would change the cursor based on whether this is a // valid place to drop. e.nativeEvent.dataTransfer.effectAllowed = verified ? "move" : "none"; - }, - handleDragOver: function (e) { + } + + handleDragOver(e) { // allow a drop by preventing default handling e.preventDefault(); - }, - render: function () { + } + + render() { // I think these might be getting styles from Webapp let dragState = "sortable-disabled"; if (this.props.dragging) { @@ -188,8 +218,8 @@ const SortableItem = createReactClass({ {this.props.component} ); - }, -}); + } +} const styles = StyleSheet.create({ sortableListItem: { diff --git a/packages/perseus-editor/src/index.ts b/packages/perseus-editor/src/index.ts index 14438825fe..73e1b5921f 100644 --- a/packages/perseus-editor/src/index.ts +++ b/packages/perseus-editor/src/index.ts @@ -11,7 +11,6 @@ export {default as Editor} from "./editor"; export {default as i18n} from "./i18n"; export {default as IframeContentRenderer} from "./iframe-content-renderer"; export {default as MultiRendererEditor} from "./multirenderer-editor"; -export {default as StatefulEditorPage} from "./stateful-editor-page"; import "./styles/perseus-editor.less"; diff --git a/packages/perseus-editor/src/stateful-editor-page.tsx b/packages/perseus-editor/src/stateful-editor-page.tsx deleted file mode 100644 index 852e285561..0000000000 --- a/packages/perseus-editor/src/stateful-editor-page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable react/no-unsafe */ -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; -import * as React from "react"; -import _ from "underscore"; - -import EditorPage from "./editor-page"; - -/* Renders an EditorPage (or an ArticleEditor) as a non-controlled component. - * - * Normally the parent of EditorPage must pass it an onChange callback and then - * respond to any changes by modifying the EditorPage props to reflect those - * changes. With StatefulEditorPage changes are stored in state so you can - * query them with serialize. - */ -const StatefulEditorPage = createReactClass({ - displayName: "StatefulEditorPage", - - propTypes: { - componentClass: PropTypes.func, - }, - - getDefaultProps: function () { - return { - componentClass: EditorPage, - }; - }, - - getInitialState: function () { - return _({}).extend(_.omit(this.props, "componentClass"), { - onChange: this.handleChange, - ref: "editor", - }); - }, - - componentDidMount: function () { - this._isMounted = true; - }, - - // getInitialState isn't called if the react component is re-rendered - // in-place on the dom, in which case this is called instead, so we - // need to update the state here. - // (This component is currently re-rendered by the "Add image" button.) - UNSAFE_componentWillReceiveProps: function (nextProps) { - this.setState( - _(nextProps).pick( - "apiOptions", - "imageUploader", - "developerMode", - "problemNum", - "previewDevice", - "frameSource", - ), - ); - }, - - componentWillUnmount: function () { - this._isMounted = false; - }, - - getSaveWarnings: function () { - // eslint-disable-next-line react/no-string-refs - return this.refs.editor.getSaveWarnings(); - }, - - serialize: function () { - // eslint-disable-next-line react/no-string-refs - return this.refs.editor.serialize(); - }, - - handleChange: function (newState, cb) { - if (this._isMounted) { - this.setState(newState, cb); - } - }, - - render: function () { - const Component = this.props.componentClass; - return ; - }, -}); - -export default StatefulEditorPage;