@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.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +26 -0
- package/dist/es/index.css +1020 -0
- package/dist/es/index.js +9 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +1020 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +6 -1
- package/package.json +16 -4
- package/src/README.md +7 -0
- package/src/__stories__/editor.stories.js +33 -0
- package/src/__stories__/section-control-button.stories.jsx +27 -0
- package/src/__testdata__/input-number_testdata.js +28 -0
- package/src/__tests__/editor_test.jsx +123 -0
- package/src/__tests__/i18n_test.jsx +585 -0
- package/src/__tests__/traversal_test.jsx +378 -0
- package/src/all-editors.js +85 -0
- package/src/all-editors_test.js +17 -0
- package/src/all-widgets.js +10 -0
- package/src/article-editor.jsx +437 -0
- package/src/components/__stories__/blur-input.stories.jsx +24 -0
- package/src/components/blur-input.jsx +62 -0
- package/src/components/checkbox.jsx +194 -0
- package/src/components/device-framer.jsx +91 -0
- package/src/components/drag-target.jsx +88 -0
- package/src/components/dropdown-option.jsx +318 -0
- package/src/components/form-wrapped-text-field.jsx +248 -0
- package/src/components/graph-settings.jsx +631 -0
- package/src/components/hover-behavior.jsx +178 -0
- package/src/components/json-editor.jsx +121 -0
- package/src/components/link.jsx +173 -0
- package/src/components/simple-button.jsx +86 -0
- package/src/components/sortable.jsx +186 -0
- package/src/components/util.js +39 -0
- package/src/components/viewport-resizer.jsx +73 -0
- package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
- package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
- package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
- package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
- package/src/diffs/__tests__/split-diff_test.js +34 -0
- package/src/diffs/__tests__/string-array-diff_test.js +74 -0
- package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
- package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
- package/src/diffs/article-diff.jsx +70 -0
- package/src/diffs/item-diff.jsx +77 -0
- package/src/diffs/renderer-diff.jsx +143 -0
- package/src/diffs/split-diff.jsx +30 -0
- package/src/diffs/string-array-diff.jsx +75 -0
- package/src/diffs/structured-item-diff.jsx +269 -0
- package/src/diffs/tags-diff.jsx +50 -0
- package/src/diffs/text-diff.jsx +194 -0
- package/src/diffs/widget-diff-performer.jsx +74 -0
- package/src/diffs/widget-diff.jsx +291 -0
- package/src/editor-page.jsx +351 -0
- package/src/editor.jsx +1364 -0
- package/src/hint-editor.jsx +447 -0
- package/src/i18n.jsx +139 -0
- package/src/iframe-content-renderer.jsx +201 -0
- package/src/index.js +27 -8
- package/src/item-editor.jsx +159 -0
- package/src/item-extras-editor.jsx +150 -0
- package/src/katex-error-view.jsx +87 -0
- package/src/multirenderer-editor.jsx +1074 -0
- package/src/section-control-button.jsx +53 -0
- package/src/stateful-article-editor.jsx +176 -0
- package/src/stateful-editor-page.jsx +90 -0
- package/src/styles/perseus-editor.less +1339 -0
- package/src/testing-widgets.js +12 -0
- package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
- package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
- package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
- package/src/widgets/categorizer-editor.jsx +103 -0
- package/src/widgets/cs-program-editor.jsx +258 -0
- package/src/widgets/definition-editor.jsx +75 -0
- package/src/widgets/dropdown-editor.jsx +201 -0
- package/src/widgets/example-graphie-widget-editor.jsx +70 -0
- package/src/widgets/example-graphie-widget.jsx +175 -0
- package/src/widgets/example-widget-editor.jsx +60 -0
- package/src/widgets/example-widget.jsx +153 -0
- package/src/widgets/explanation-editor.jsx +89 -0
- package/src/widgets/expression-editor.jsx +560 -0
- package/src/widgets/graded-group-editor.jsx +162 -0
- package/src/widgets/graded-group-set-editor.jsx +101 -0
- package/src/widgets/grapher-editor.jsx +154 -0
- package/src/widgets/group-editor.jsx +83 -0
- package/src/widgets/iframe-editor.jsx +200 -0
- package/src/widgets/image-editor.jsx +326 -0
- package/src/widgets/input-number-editor.jsx +269 -0
- package/src/widgets/interaction/constraint-editor.jsx +147 -0
- package/src/widgets/interaction-editor.jsx +1389 -0
- package/src/widgets/interactive-graph-editor.jsx +322 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
- package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
- package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
- package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
- package/src/widgets/label-image/answer-choices.jsx +231 -0
- package/src/widgets/label-image/behavior.jsx +92 -0
- package/src/widgets/label-image/marker.jsx +292 -0
- package/src/widgets/label-image/question-markers.jsx +181 -0
- package/src/widgets/label-image/select-image.jsx +86 -0
- package/src/widgets/label-image-editor.jsx +257 -0
- package/src/widgets/lights-puzzle-editor.jsx +267 -0
- package/src/widgets/matcher-editor.jsx +147 -0
- package/src/widgets/matrix-editor.jsx +155 -0
- package/src/widgets/measurer-editor.jsx +245 -0
- package/src/widgets/molecule-editor.jsx +63 -0
- package/src/widgets/number-line-editor.jsx +498 -0
- package/src/widgets/numeric-input-editor.jsx +554 -0
- package/src/widgets/orderer-editor.jsx +166 -0
- package/src/widgets/passage-editor.jsx +115 -0
- package/src/widgets/passage-ref-editor.jsx +77 -0
- package/src/widgets/passage-ref-target-editor.jsx +47 -0
- package/src/widgets/plotter-editor.jsx +537 -0
- package/src/widgets/radio/editor.jsx +398 -0
- package/src/widgets/reaction-diagram-editor.jsx +127 -0
- package/src/widgets/sequence-editor.jsx +205 -0
- package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
- package/src/widgets/simple-markdown-tester.jsx +91 -0
- package/src/widgets/simulator-editor.jsx +144 -0
- package/src/widgets/sorter-editor.jsx +100 -0
- package/src/widgets/table-editor.jsx +164 -0
- package/src/widgets/transformer-editor.jsx +1006 -0
- package/src/widgets/unit-editor.jsx +293 -0
- package/src/widgets/video-editor.jsx +74 -0
- 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;
|