@khanacademy/perseus-editor 0.0.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/es/index.css +1020 -0
  4. package/dist/es/index.js +9 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +1020 -0
  7. package/dist/index.js +9 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +6 -1
  10. package/package.json +16 -4
  11. package/src/README.md +7 -0
  12. package/src/__stories__/editor.stories.js +33 -0
  13. package/src/__stories__/section-control-button.stories.jsx +27 -0
  14. package/src/__testdata__/input-number_testdata.js +28 -0
  15. package/src/__tests__/editor_test.jsx +123 -0
  16. package/src/__tests__/i18n_test.jsx +585 -0
  17. package/src/__tests__/traversal_test.jsx +378 -0
  18. package/src/all-editors.js +85 -0
  19. package/src/all-editors_test.js +17 -0
  20. package/src/all-widgets.js +10 -0
  21. package/src/article-editor.jsx +437 -0
  22. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  23. package/src/components/blur-input.jsx +62 -0
  24. package/src/components/checkbox.jsx +194 -0
  25. package/src/components/device-framer.jsx +91 -0
  26. package/src/components/drag-target.jsx +88 -0
  27. package/src/components/dropdown-option.jsx +318 -0
  28. package/src/components/form-wrapped-text-field.jsx +248 -0
  29. package/src/components/graph-settings.jsx +631 -0
  30. package/src/components/hover-behavior.jsx +178 -0
  31. package/src/components/json-editor.jsx +121 -0
  32. package/src/components/link.jsx +173 -0
  33. package/src/components/simple-button.jsx +86 -0
  34. package/src/components/sortable.jsx +186 -0
  35. package/src/components/util.js +39 -0
  36. package/src/components/viewport-resizer.jsx +73 -0
  37. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  38. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  39. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  40. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  41. package/src/diffs/__tests__/split-diff_test.js +34 -0
  42. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  43. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  44. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  45. package/src/diffs/article-diff.jsx +70 -0
  46. package/src/diffs/item-diff.jsx +77 -0
  47. package/src/diffs/renderer-diff.jsx +143 -0
  48. package/src/diffs/split-diff.jsx +30 -0
  49. package/src/diffs/string-array-diff.jsx +75 -0
  50. package/src/diffs/structured-item-diff.jsx +269 -0
  51. package/src/diffs/tags-diff.jsx +50 -0
  52. package/src/diffs/text-diff.jsx +194 -0
  53. package/src/diffs/widget-diff-performer.jsx +74 -0
  54. package/src/diffs/widget-diff.jsx +291 -0
  55. package/src/editor-page.jsx +351 -0
  56. package/src/editor.jsx +1364 -0
  57. package/src/hint-editor.jsx +447 -0
  58. package/src/i18n.jsx +139 -0
  59. package/src/iframe-content-renderer.jsx +201 -0
  60. package/src/index.js +27 -8
  61. package/src/item-editor.jsx +159 -0
  62. package/src/item-extras-editor.jsx +150 -0
  63. package/src/katex-error-view.jsx +87 -0
  64. package/src/multirenderer-editor.jsx +1074 -0
  65. package/src/section-control-button.jsx +53 -0
  66. package/src/stateful-article-editor.jsx +176 -0
  67. package/src/stateful-editor-page.jsx +90 -0
  68. package/src/styles/perseus-editor.less +1339 -0
  69. package/src/testing-widgets.js +12 -0
  70. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  71. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  72. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  73. package/src/widgets/categorizer-editor.jsx +103 -0
  74. package/src/widgets/cs-program-editor.jsx +258 -0
  75. package/src/widgets/definition-editor.jsx +75 -0
  76. package/src/widgets/dropdown-editor.jsx +201 -0
  77. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  78. package/src/widgets/example-graphie-widget.jsx +175 -0
  79. package/src/widgets/example-widget-editor.jsx +60 -0
  80. package/src/widgets/example-widget.jsx +153 -0
  81. package/src/widgets/explanation-editor.jsx +89 -0
  82. package/src/widgets/expression-editor.jsx +560 -0
  83. package/src/widgets/graded-group-editor.jsx +162 -0
  84. package/src/widgets/graded-group-set-editor.jsx +101 -0
  85. package/src/widgets/grapher-editor.jsx +154 -0
  86. package/src/widgets/group-editor.jsx +83 -0
  87. package/src/widgets/iframe-editor.jsx +200 -0
  88. package/src/widgets/image-editor.jsx +326 -0
  89. package/src/widgets/input-number-editor.jsx +269 -0
  90. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  91. package/src/widgets/interaction-editor.jsx +1389 -0
  92. package/src/widgets/interactive-graph-editor.jsx +322 -0
  93. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  94. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  95. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  96. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  97. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  98. package/src/widgets/label-image/answer-choices.jsx +231 -0
  99. package/src/widgets/label-image/behavior.jsx +92 -0
  100. package/src/widgets/label-image/marker.jsx +292 -0
  101. package/src/widgets/label-image/question-markers.jsx +181 -0
  102. package/src/widgets/label-image/select-image.jsx +86 -0
  103. package/src/widgets/label-image-editor.jsx +257 -0
  104. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  105. package/src/widgets/matcher-editor.jsx +147 -0
  106. package/src/widgets/matrix-editor.jsx +155 -0
  107. package/src/widgets/measurer-editor.jsx +245 -0
  108. package/src/widgets/molecule-editor.jsx +63 -0
  109. package/src/widgets/number-line-editor.jsx +498 -0
  110. package/src/widgets/numeric-input-editor.jsx +554 -0
  111. package/src/widgets/orderer-editor.jsx +166 -0
  112. package/src/widgets/passage-editor.jsx +115 -0
  113. package/src/widgets/passage-ref-editor.jsx +77 -0
  114. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  115. package/src/widgets/plotter-editor.jsx +537 -0
  116. package/src/widgets/radio/editor.jsx +398 -0
  117. package/src/widgets/reaction-diagram-editor.jsx +127 -0
  118. package/src/widgets/sequence-editor.jsx +205 -0
  119. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  120. package/src/widgets/simple-markdown-tester.jsx +91 -0
  121. package/src/widgets/simulator-editor.jsx +144 -0
  122. package/src/widgets/sorter-editor.jsx +100 -0
  123. package/src/widgets/table-editor.jsx +164 -0
  124. package/src/widgets/transformer-editor.jsx +1006 -0
  125. package/src/widgets/unit-editor.jsx +293 -0
  126. package/src/widgets/video-editor.jsx +74 -0
  127. package/src/__tests__/index.test.js +0 -15
@@ -0,0 +1,186 @@
1
+ // @noflow
2
+ import createReactClass from "create-react-class";
3
+ import PropTypes from "prop-types";
4
+ import * as React from "react";
5
+ import ReactDOM from "react-dom";
6
+
7
+ const PT = PropTypes;
8
+
9
+ // Takes an array of components to sort
10
+ // eslint-disable-next-line react/no-unsafe
11
+ const SortableArea = createReactClass({
12
+ propTypes: {
13
+ className: PT.string,
14
+ components: PT.arrayOf(PT.node).isRequired,
15
+ onReorder: PT.func.isRequired,
16
+ style: PT.any,
17
+ verify: PT.func,
18
+ },
19
+ getDefaultProps: function () {
20
+ return {verify: () => true};
21
+ },
22
+ getInitialState: function () {
23
+ return {
24
+ // index of the component being dragged
25
+ dragging: null,
26
+ components: this.props.components,
27
+ };
28
+ },
29
+ // Firefox refuses to drag an element unless you set data on it. Hackily
30
+ // add data each time an item is dragged.
31
+ componentDidMount: function () {
32
+ this._setDragEvents();
33
+ },
34
+ UNSAFE_componentWillReceiveProps: function (nextProps) {
35
+ this.setState({components: nextProps.components});
36
+ },
37
+ componentDidUpdate: function () {
38
+ this._setDragEvents();
39
+ },
40
+ // Alternatively send each handler to each component individually,
41
+ // partially applied
42
+ onDragStart: function (startIndex) {
43
+ this.setState({dragging: startIndex});
44
+ },
45
+ onDrop: function () {
46
+ // tell the parent component
47
+ this.setState({dragging: null});
48
+ this.props.onReorder(this.state.components);
49
+ },
50
+ onDragEnter: function (enterIndex) {
51
+ // When a label is first dragged it triggers a dragEnter with itself,
52
+ // which we don't care about.
53
+ if (this.state.dragging === enterIndex) {
54
+ return;
55
+ }
56
+
57
+ const newComponents = this.state.components.slice();
58
+
59
+ // splice the tab out of its old position
60
+ const removed = newComponents.splice(this.state.dragging, 1);
61
+ // ... and into its new position
62
+ newComponents.splice(enterIndex, 0, removed[0]);
63
+
64
+ const verified = this.props.verify(newComponents);
65
+ if (verified) {
66
+ this.setState({
67
+ dragging: enterIndex,
68
+ components: newComponents,
69
+ });
70
+ }
71
+ return verified;
72
+ },
73
+ _listenEvent: function (e) {
74
+ e.dataTransfer.setData("hackhackhack", "because browsers!");
75
+ },
76
+ _cancelEvent: function (e) {
77
+ // prevent the browser from redirecting to 'because browsers!'
78
+ e.preventDefault();
79
+ },
80
+ _setDragEvents: function () {
81
+ this._dragItems = this._dragItems || [];
82
+ const items =
83
+ ReactDOM.findDOMNode(this).querySelectorAll("[draggable=true]");
84
+
85
+ const oldItems = [];
86
+ const newItems = [];
87
+
88
+ for (let i = 0; i < this._dragItems.length; i++) {
89
+ const item = this._dragItems[i];
90
+ if (items.indexOf(item) < 0) {
91
+ oldItems.push(item);
92
+ }
93
+ }
94
+
95
+ for (let i = 0; i < items.length; i++) {
96
+ const item = items[i];
97
+ if (this._dragItems.indexOf(item) < 0) {
98
+ newItems.push(item);
99
+ }
100
+ }
101
+
102
+ for (let i = 0; i < newItems.length; i++) {
103
+ const dragItem = newItems[i];
104
+ dragItem.addEventListener("dragstart", this._listenEvent);
105
+ dragItem.addEventListener("drop", this._cancelEvent);
106
+ }
107
+
108
+ for (let i = 0; i < oldItems.length; i++) {
109
+ const dragItem = oldItems[i];
110
+ dragItem.removeEventListener("dragstart", this._listenEvent);
111
+ dragItem.removeEventListener("drop", this._cancelEvent);
112
+ }
113
+ },
114
+ render: function () {
115
+ const sortables = this.state.components.map((component, index) => (
116
+ <SortableItem
117
+ index={index}
118
+ component={component}
119
+ area={this}
120
+ key={component.key}
121
+ draggable={component.props.draggable}
122
+ dragging={index === this.state.dragging}
123
+ />
124
+ ));
125
+ return (
126
+ <ol className={this.props.className} style={this.props.style}>
127
+ {sortables}
128
+ </ol>
129
+ );
130
+ },
131
+ });
132
+
133
+ // An individual sortable item
134
+ const SortableItem = createReactClass({
135
+ propTypes: {
136
+ area: PT.shape({
137
+ onDragEnter: PT.func.isRequired,
138
+ onDragStart: PT.func.isRequired,
139
+ onDrop: PT.func.isRequired,
140
+ }),
141
+ component: PT.node.isRequired,
142
+ dragging: PT.bool.isRequired,
143
+ draggable: PT.bool.isRequired,
144
+ index: PT.number.isRequired,
145
+ },
146
+ handleDragStart: function (e) {
147
+ e.nativeEvent.dataTransfer.effectAllowed = "move";
148
+ this.props.area.onDragStart(this.props.index);
149
+ },
150
+ handleDrop: function () {
151
+ this.props.area.onDrop(this.props.index);
152
+ },
153
+ handleDragEnter: function (e) {
154
+ const verified = this.props.area.onDragEnter(this.props.index);
155
+ // Ideally this would change the cursor based on whether this is a
156
+ // valid place to drop.
157
+ e.nativeEvent.dataTransfer.effectAllowed = verified ? "move" : "none";
158
+ },
159
+ handleDragOver: function (e) {
160
+ // allow a drop by preventing default handling
161
+ e.preventDefault();
162
+ },
163
+ render: function () {
164
+ let dragState = "sortable-disabled";
165
+ if (this.props.dragging) {
166
+ dragState = "sortable-dragging";
167
+ } else if (this.props.draggable) {
168
+ dragState = "sortable-enabled";
169
+ }
170
+
171
+ return (
172
+ <li
173
+ draggable={this.props.draggable}
174
+ className={dragState}
175
+ onDragStart={this.handleDragStart}
176
+ onDrop={this.handleDrop}
177
+ onDragEnter={this.handleDragEnter}
178
+ onDragOver={this.handleDragOver}
179
+ >
180
+ {this.props.component}
181
+ </li>
182
+ );
183
+ },
184
+ });
185
+
186
+ export default SortableArea;
@@ -0,0 +1,39 @@
1
+ // @flow
2
+ export function focusWithChromeStickyFocusBugWorkaround(element: Element) {
3
+ // NOTE(marksandstrom) Chrome sticky focus bug.
4
+ //
5
+ // There is a bug in Chrome related to sticky positioning and
6
+ // focus: When a child of an element with sticky positioning is
7
+ // focused with the focus() method, the scrollable area
8
+ // containing that element will scroll to the top (even though
9
+ // the element is already visble on the page). For a dropdown
10
+ // this means that the area will be scrolled to the top when
11
+ // the user interacts with the dropdown (since we actively
12
+ // manage dropdown header and option focus for keyboard users).
13
+ //
14
+ // To work around this bug the `preventScroll` focus option is
15
+ // used.
16
+ //
17
+ // Using `focusOptions` is an experimental browser feature
18
+ // "that should not be used in production code". However, since
19
+ // Chrome is the only browser we support that exhibits this
20
+ // buggy behavior and using `focusOptions` works around the
21
+ // bug, it is perfectly reasonable to use the feature in this
22
+ // case. We are not relying on the effect of the
23
+ // `preventScroll` option otherwise.
24
+ //
25
+ // focusOptions documentation:
26
+ //
27
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
28
+ //
29
+ // Note that there could be cases where we want default focus() behavior.
30
+ // One that comes to mind is a dropdown with a long list of options that
31
+ // might be rendered off screen when opened. With this workaround the
32
+ // selected option would not be scrolled into view if the option is near
33
+ // the end of the list. We're not aware of any uses of the dropdown where
34
+ // this is an issue, however, so there isn't currently a way to out of the
35
+ // `preventScroll` behavior.
36
+
37
+ // $FlowIgnore[prop-missing]
38
+ element.focus({preventScroll: true});
39
+ }
@@ -0,0 +1,73 @@
1
+ // @flow
2
+ /**
3
+ * A component that displays controls for choosing a viewport size.
4
+ * Renders three buttons: "Phone", "Tablet", and "Desktop".
5
+ */
6
+ import {components, constants, icons} from "@khanacademy/perseus";
7
+ import PropTypes from "prop-types";
8
+ import * as React from "react";
9
+
10
+ import type {DeviceType} from "@khanacademy/perseus";
11
+
12
+ const {ButtonGroup, InlineIcon} = components;
13
+ const {devices} = constants;
14
+ const {iconDesktop, iconMobilePhone, iconTablet} = icons;
15
+
16
+ type Props = {|
17
+ deviceType: DeviceType,
18
+ onViewportSizeChanged: (DeviceType) => mixed,
19
+ |};
20
+
21
+ class ViewportResizer extends React.Component<Props> {
22
+ static propTypes = {
23
+ // The current device type that is selected.
24
+ deviceType: PropTypes.string.isRequired,
25
+ // A callback that is passed (width, height) as the dimensions of the
26
+ // viewport to resize to.
27
+ onViewportSizeChanged: PropTypes.func.isRequired,
28
+ };
29
+
30
+ handleChange: (value: DeviceType) => void = (value: DeviceType) => {
31
+ this.props.onViewportSizeChanged(value);
32
+ };
33
+
34
+ render(): React.Element<"span"> {
35
+ const phoneButtonContents = (
36
+ <span>
37
+ <InlineIcon {...iconMobilePhone} /> Phone
38
+ </span>
39
+ );
40
+ const tabletButtonContents = (
41
+ <span>
42
+ <InlineIcon {...iconTablet} /> Tablet
43
+ </span>
44
+ );
45
+ const desktopButtonContents = (
46
+ <span>
47
+ <InlineIcon {...iconDesktop} /> Desktop
48
+ </span>
49
+ );
50
+
51
+ // TODO(david): Allow input of custom viewport sizes.
52
+ return (
53
+ <span className="viewport-resizer">
54
+ Viewport:{" "}
55
+ <ButtonGroup
56
+ value={this.props.deviceType}
57
+ allowEmpty={false}
58
+ buttons={[
59
+ {value: devices.PHONE, content: phoneButtonContents},
60
+ {value: devices.TABLET, content: tabletButtonContents},
61
+ {
62
+ value: devices.DESKTOP,
63
+ content: desktopButtonContents,
64
+ },
65
+ ]}
66
+ onChange={this.handleChange}
67
+ />
68
+ </span>
69
+ );
70
+ }
71
+ }
72
+
73
+ export default ViewportResizer;
@@ -0,0 +1,8 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ const Wrapper = ({children}: {|children: React.Node|}): React.Node => {
5
+ return <div className="perseus-diff">{children}</div>;
6
+ };
7
+
8
+ export default Wrapper;