@khanacademy/perseus-editor 0.1.1 → 0.2.0
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/CHANGELOG.md +26 -0
- package/dist/es/index.js +2 -2
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +14 -6
- package/src/article-editor.jsx +3 -2
- package/src/components/link.jsx +4 -4
- package/src/diffs/string-array-diff.jsx +1 -0
- package/src/diffs/text-diff.jsx +1 -0
- package/src/editor-page.jsx +5 -2
- package/src/hint-editor.jsx +6 -4
- package/src/iframe-content-renderer.jsx +49 -67
- package/src/item-editor.jsx +3 -2
- package/src/multirenderer-editor.jsx +1 -0
- package/src/widgets/expression-editor.jsx +1 -0
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"name": "@khanacademy/perseus-editor",
|
|
3
|
+
"description": "Perseus editors",
|
|
4
|
+
"author": "Khan Academy",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"version": "0.2.0",
|
|
2
7
|
"publishConfig": {
|
|
3
8
|
"access": "public"
|
|
4
9
|
},
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/Khan/perseus-editor.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Khan/perseus/issues"
|
|
16
|
+
},
|
|
8
17
|
"module": "dist/es/index.js",
|
|
9
18
|
"main": "dist/index.js",
|
|
10
19
|
"source": "src/index.js",
|
|
@@ -12,7 +21,7 @@
|
|
|
12
21
|
"test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'"
|
|
13
22
|
},
|
|
14
23
|
"dependencies": {
|
|
15
|
-
"@khanacademy/perseus": "^0.2.
|
|
24
|
+
"@khanacademy/perseus": "^0.2.3"
|
|
16
25
|
},
|
|
17
26
|
"devDependencies": {
|
|
18
27
|
"@khanacademy/wonder-blocks-i18n": "^1.2.3",
|
|
@@ -37,6 +46,5 @@
|
|
|
37
46
|
"react-dom": "16.14.0",
|
|
38
47
|
"underscore": "^1.4.4"
|
|
39
48
|
},
|
|
40
|
-
"
|
|
41
|
-
"license": "MIT"
|
|
49
|
+
"keywords": []
|
|
42
50
|
}
|
package/src/article-editor.jsx
CHANGED
|
@@ -47,8 +47,9 @@ type DefaultProps = {|
|
|
|
47
47
|
type PerseusArticleEditorProps = {|
|
|
48
48
|
...DefaultProps,
|
|
49
49
|
apiOptions?: APIOptions,
|
|
50
|
-
frameSource: string,
|
|
51
50
|
imageUploader?: (string, (string) => mixed) => mixed,
|
|
51
|
+
// URL of the route to show on initial load of the preview frames.
|
|
52
|
+
previewURL: string,
|
|
52
53
|
...Changeable.ChangeableProps,
|
|
53
54
|
|};
|
|
54
55
|
|
|
@@ -285,10 +286,10 @@ export default class ArticleEditor extends React.Component<
|
|
|
285
286
|
<IframeContentRenderer
|
|
286
287
|
ref={"frame-" + i}
|
|
287
288
|
key={this.props.screen}
|
|
288
|
-
content={this.props.frameSource}
|
|
289
289
|
datasetKey="mobile"
|
|
290
290
|
datasetValue={isMobile}
|
|
291
291
|
seamless={nochrome}
|
|
292
|
+
url={this.props.previewURL}
|
|
292
293
|
/>
|
|
293
294
|
</DeviceFramer>
|
|
294
295
|
);
|
package/src/components/link.jsx
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
import {StyleSheet, css} from "aphrodite";
|
|
9
9
|
import * as React from "react";
|
|
10
10
|
|
|
11
|
+
import type {CSSProperties} from "aphrodite";
|
|
12
|
+
|
|
11
13
|
// TODO(diedra): This is terrible! If we don't have a href for a link, it should
|
|
12
14
|
// be a button instead. We just need to make sure we override the default browser
|
|
13
15
|
// button styling (backgroundColor, border, and padding).
|
|
14
16
|
const DEFAULT_HREF = "javascript:void(0)";
|
|
15
17
|
|
|
16
|
-
type AphroditeStyle = any;
|
|
17
|
-
|
|
18
18
|
type DefaultProps = {|
|
|
19
19
|
element: string | React.AbstractComponent<any>,
|
|
20
20
|
// Whether the link should appear in a 'highlighted' state. In
|
|
@@ -22,7 +22,7 @@ type DefaultProps = {|
|
|
|
22
22
|
highlighted: boolean,
|
|
23
23
|
href: string,
|
|
24
24
|
// Pass through styles but also add our own to normalize
|
|
25
|
-
style:
|
|
25
|
+
style: CSSProperties | Array<CSSProperties>,
|
|
26
26
|
|};
|
|
27
27
|
|
|
28
28
|
type LinkProps = {|
|
|
@@ -70,7 +70,7 @@ class Link extends React.Component<LinkProps> {
|
|
|
70
70
|
static defaultProps: DefaultProps = {
|
|
71
71
|
highlighted: false,
|
|
72
72
|
href: DEFAULT_HREF,
|
|
73
|
-
style: ([]: Array<
|
|
73
|
+
style: ([]: Array<CSSProperties>),
|
|
74
74
|
element: "a",
|
|
75
75
|
};
|
|
76
76
|
|
package/src/diffs/text-diff.jsx
CHANGED
package/src/editor-page.jsx
CHANGED
|
@@ -63,6 +63,9 @@ type Props = {|
|
|
|
63
63
|
|
|
64
64
|
// Initial value of the question being edited
|
|
65
65
|
question?: $FlowFixMe,
|
|
66
|
+
|
|
67
|
+
// URL of the route to show on initial load of the preview frames.
|
|
68
|
+
previewURL: string,
|
|
66
69
|
|};
|
|
67
70
|
|
|
68
71
|
type PerseusJson = {|
|
|
@@ -325,7 +328,7 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
325
328
|
gradeMessage={this.state.gradeMessage}
|
|
326
329
|
deviceType={this.props.previewDevice}
|
|
327
330
|
apiOptions={deviceBasedApiOptions}
|
|
328
|
-
|
|
331
|
+
previewURL={this.props.previewURL}
|
|
329
332
|
/>
|
|
330
333
|
)}
|
|
331
334
|
|
|
@@ -339,7 +342,7 @@ class EditorPage extends React.Component<Props, State> {
|
|
|
339
342
|
onChange={this.handleChange}
|
|
340
343
|
deviceType={this.props.previewDevice}
|
|
341
344
|
apiOptions={deviceBasedApiOptions}
|
|
342
|
-
|
|
345
|
+
previewURL={this.props.previewURL}
|
|
343
346
|
highlightLint={this.state.highlightLint}
|
|
344
347
|
/>
|
|
345
348
|
)}
|
package/src/hint-editor.jsx
CHANGED
|
@@ -165,7 +165,6 @@ type CombinedHintEditorProps = {|
|
|
|
165
165
|
itemId?: string,
|
|
166
166
|
apiOptions?: APIOptions,
|
|
167
167
|
deviceType: DeviceType,
|
|
168
|
-
frameSource: string,
|
|
169
168
|
imageUploader?: ImageUploader,
|
|
170
169
|
highlightLint?: boolean,
|
|
171
170
|
isLast: boolean,
|
|
@@ -173,6 +172,8 @@ type CombinedHintEditorProps = {|
|
|
|
173
172
|
hint: Hint,
|
|
174
173
|
pos: number, // position
|
|
175
174
|
contentPaths: $ReadOnlyArray<string>,
|
|
175
|
+
// URL of the route to show on initial load of the preview frames.
|
|
176
|
+
previewURL: string,
|
|
176
177
|
onMove: (direction: number) => mixed,
|
|
177
178
|
onRemove: () => mixed,
|
|
178
179
|
onChange: ChangeHandler,
|
|
@@ -264,10 +265,10 @@ class CombinedHintEditor extends React.Component<CombinedHintEditorProps> {
|
|
|
264
265
|
<IframeContentRenderer
|
|
265
266
|
// eslint-disable-next-line react/no-string-refs
|
|
266
267
|
ref="frame"
|
|
267
|
-
content={this.props.frameSource}
|
|
268
268
|
datasetKey="mobile"
|
|
269
269
|
datasetValue={isMobile}
|
|
270
270
|
seamless={true}
|
|
271
|
+
url={this.props.previewURL}
|
|
271
272
|
/>
|
|
272
273
|
</DeviceFramer>
|
|
273
274
|
</div>
|
|
@@ -279,10 +280,11 @@ class CombinedHintEditor extends React.Component<CombinedHintEditorProps> {
|
|
|
279
280
|
type CombinedHintsEditorProps = {|
|
|
280
281
|
apiOptions?: APIOptions,
|
|
281
282
|
deviceType: DeviceType,
|
|
282
|
-
frameSource: string,
|
|
283
283
|
imageUploader?: ImageUploader,
|
|
284
284
|
highlightLint?: boolean,
|
|
285
285
|
hints: $ReadOnlyArray<Hint>,
|
|
286
|
+
// URL of the route to show on initial load of the preview frames.
|
|
287
|
+
previewURL: string,
|
|
286
288
|
onChange: ChangeHandler,
|
|
287
289
|
|
|
288
290
|
// The content ID of the AssessmentItem being edited. It may not be set
|
|
@@ -414,8 +416,8 @@ class CombinedHintsEditor extends React.Component<CombinedHintsEditorProps> {
|
|
|
414
416
|
onMove={this.handleHintMove.bind(this, i)}
|
|
415
417
|
deviceType={this.props.deviceType}
|
|
416
418
|
apiOptions={this.props.apiOptions}
|
|
417
|
-
frameSource={this.props.frameSource}
|
|
418
419
|
highlightLint={this.props.highlightLint}
|
|
420
|
+
previewURL={this.props.previewURL}
|
|
419
421
|
// TODO(CP-4838): what should be passed here?
|
|
420
422
|
contentPaths={[]}
|
|
421
423
|
/>
|
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
* its requestIframeData function in the parent, which triggers the parent to
|
|
13
13
|
* send the current data.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
15
|
import {Log} from "@khanacademy/perseus";
|
|
17
|
-
import createReactClass from "create-react-class";
|
|
18
|
-
import PropTypes from "prop-types";
|
|
19
16
|
import * as React from "react";
|
|
20
17
|
|
|
21
18
|
let nextIframeID = 0;
|
|
@@ -52,25 +49,32 @@ window.addEventListener("message", (event) => {
|
|
|
52
49
|
}
|
|
53
50
|
});
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
type Props = {|
|
|
53
|
+
// The HTML content to render to the iframe
|
|
54
|
+
// content?: string,
|
|
55
|
+
|
|
56
|
+
// The URL that the iframe should load
|
|
57
|
+
url: string,
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
content: PropTypes.string.isRequired,
|
|
59
|
+
// The data-* suffix for passing information to the iframe's JS
|
|
60
|
+
datasetKey: string,
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// The value of the data-* attribute
|
|
63
|
+
datasetValue: $FlowFixMe,
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Whether to make the iframe's height match its content's height,
|
|
66
|
+
// used to prevent scrolling inside the iframe.
|
|
67
|
+
seamless: boolean,
|
|
68
|
+
|};
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
class IframeContentRenderer extends React.Component<Props> {
|
|
71
|
+
_frame: ?HTMLIFrameElement;
|
|
72
|
+
_isMounted: boolean;
|
|
73
|
+
_lastData: $FlowFixMe;
|
|
74
|
+
_lastHeight: number;
|
|
75
|
+
iframeID: number;
|
|
72
76
|
|
|
73
|
-
componentDidMount
|
|
77
|
+
componentDidMount() {
|
|
74
78
|
// TODO(scottgrant): This is a hack to remove the deprecated call to
|
|
75
79
|
// this.isMounted() but is still considered an anti-pattern.
|
|
76
80
|
this._isMounted = true;
|
|
@@ -90,17 +94,16 @@ const IframeContentRenderer: $FlowFixMe = createReactClass({
|
|
|
90
94
|
this.refs.container.style.height = height + "px";
|
|
91
95
|
}
|
|
92
96
|
};
|
|
93
|
-
}
|
|
97
|
+
}
|
|
94
98
|
|
|
95
|
-
shouldComponentUpdate:
|
|
99
|
+
shouldComponentUpdate(nextProps: Props): boolean {
|
|
96
100
|
return (
|
|
97
|
-
nextProps.content !== this.props.content ||
|
|
98
101
|
nextProps.datasetValue !== this.props.datasetValue ||
|
|
99
102
|
nextProps.seamless !== this.props.seamless
|
|
100
103
|
);
|
|
101
|
-
}
|
|
104
|
+
}
|
|
102
105
|
|
|
103
|
-
componentDidUpdate:
|
|
106
|
+
componentDidUpdate(prevProps: Props) {
|
|
104
107
|
if (!this.props.seamless) {
|
|
105
108
|
// eslint-disable-next-line react/no-string-refs
|
|
106
109
|
this.refs.container.style.height = "100%";
|
|
@@ -109,41 +112,38 @@ const IframeContentRenderer: $FlowFixMe = createReactClass({
|
|
|
109
112
|
this.refs.container.style.height = this._lastHeight + "px";
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
if (
|
|
113
|
-
prevProps.content !== this.props.content ||
|
|
114
|
-
prevProps.datasetValue !== this.props.datasetValue
|
|
115
|
-
) {
|
|
115
|
+
if (prevProps.datasetValue !== this.props.datasetValue) {
|
|
116
116
|
// Not just a change in seamless
|
|
117
117
|
this._prepareFrame();
|
|
118
118
|
}
|
|
119
|
-
}
|
|
119
|
+
}
|
|
120
120
|
|
|
121
|
-
componentWillUnmount
|
|
121
|
+
componentWillUnmount() {
|
|
122
122
|
requestIframeData[this.iframeID] = null;
|
|
123
123
|
updateIframeHeight[this.iframeID] = null;
|
|
124
124
|
|
|
125
125
|
this._isMounted = false;
|
|
126
|
-
}
|
|
126
|
+
}
|
|
127
127
|
|
|
128
|
-
_prepareFrame
|
|
128
|
+
_prepareFrame() {
|
|
129
129
|
// Don't initialize the iframe until the page has loaded
|
|
130
130
|
if (this._frame) {
|
|
131
131
|
// eslint-disable-next-line react/no-string-refs
|
|
132
132
|
this.refs.container.removeChild(this._frame);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
const frame = document.createElement("iframe");
|
|
136
|
+
frame.style.width = "100%";
|
|
137
|
+
frame.style.height = "100%";
|
|
138
|
+
frame.src = this.props.url;
|
|
138
139
|
|
|
139
140
|
if (this.props.datasetKey) {
|
|
140
141
|
// If the user has specified a data-* attribute to place on the
|
|
141
142
|
// iframe, we set it here. Right now, this is used to
|
|
142
143
|
// communicate if the iframe should be enabling touch emulation.
|
|
143
|
-
|
|
144
|
-
this.props.datasetValue;
|
|
144
|
+
frame.dataset[this.props.datasetKey] = this.props.datasetValue;
|
|
145
145
|
}
|
|
146
|
-
|
|
146
|
+
frame.dataset.id = String(this.iframeID);
|
|
147
147
|
|
|
148
148
|
if (this.props.seamless) {
|
|
149
149
|
// The seamless prop is the same as the "nochrome" prop that
|
|
@@ -152,50 +152,32 @@ const IframeContentRenderer: $FlowFixMe = createReactClass({
|
|
|
152
152
|
// for lint indicators in the right margin. We use the dataset
|
|
153
153
|
// as above to pass this information on to the perseus-frame
|
|
154
154
|
// component inside the iframe
|
|
155
|
-
|
|
155
|
+
frame.dataset.lintGutter = "true";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
// To make sure the value of location.href inside the iframe is the
|
|
159
|
-
// same as the location of the parent, we wait for the iframe to
|
|
160
|
-
// load before writing contents. Without the wait, the location
|
|
161
|
-
// inside the iframe becomes "about:blank", which causes problems
|
|
162
|
-
// with loading $LAB.
|
|
163
|
-
this._frame.onload = () => {
|
|
164
|
-
this._frame.onload = null;
|
|
165
|
-
|
|
166
|
-
// To prevent an issue with the contents of the iframe not being
|
|
167
|
-
// loaded properly, where the javascript inside the iframe is
|
|
168
|
-
// not executed, we push the content window write to the end
|
|
169
|
-
// of the event queue.
|
|
170
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API
|
|
171
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
172
|
-
setTimeout(() => {
|
|
173
|
-
this._frame.contentWindow.document.open();
|
|
174
|
-
this._frame.contentWindow.document.write(this.props.content);
|
|
175
|
-
this._frame.contentWindow.document.close();
|
|
176
|
-
});
|
|
177
|
-
};
|
|
178
|
-
|
|
179
158
|
// eslint-disable-next-line react/no-string-refs
|
|
180
|
-
this.refs.container.appendChild(
|
|
181
|
-
|
|
159
|
+
this.refs.container.appendChild(frame);
|
|
160
|
+
|
|
161
|
+
this._frame = frame;
|
|
162
|
+
}
|
|
182
163
|
|
|
183
|
-
sendNewData:
|
|
184
|
-
|
|
164
|
+
sendNewData(data: $FlowFixMe) {
|
|
165
|
+
const frame = this._frame;
|
|
166
|
+
if (this._isMounted && data && frame?.contentWindow) {
|
|
185
167
|
this._lastData = data;
|
|
186
168
|
|
|
187
169
|
// We can't use JSON.stringify/parse for this because the apiOptions
|
|
188
170
|
// includes the functions GroupMetadataEditor, groupAnnotator,
|
|
189
171
|
// onFocusChange, and onInputError.
|
|
190
172
|
window.iframeDataStore[this.iframeID] = data;
|
|
191
|
-
|
|
173
|
+
frame.contentWindow.postMessage(this.iframeID, "*");
|
|
192
174
|
}
|
|
193
|
-
}
|
|
175
|
+
}
|
|
194
176
|
|
|
195
|
-
render:
|
|
177
|
+
render(): React.Node {
|
|
196
178
|
// eslint-disable-next-line react/no-string-refs
|
|
197
179
|
return <div ref="container" style={{width: "100%", height: "100%"}} />;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
200
182
|
|
|
201
183
|
export default IframeContentRenderer;
|
package/src/item-editor.jsx
CHANGED
|
@@ -19,12 +19,13 @@ const ITEM_DATA_VERSION = itemDataVersion;
|
|
|
19
19
|
type Props = {|
|
|
20
20
|
apiOptions?: APIOptions,
|
|
21
21
|
deviceType?: string,
|
|
22
|
-
frameSource: string,
|
|
23
22
|
gradeMessage?: string,
|
|
24
23
|
imageUploader?: ImageUploader,
|
|
25
24
|
wasAnswered?: boolean,
|
|
26
25
|
question?: $FlowFixMe,
|
|
27
26
|
answerArea?: $FlowFixMe,
|
|
27
|
+
// URL of the route to show on initial load of the preview frames.
|
|
28
|
+
previewURL: string,
|
|
28
29
|
onChange: ChangeHandler,
|
|
29
30
|
|
|
30
31
|
// The content ID of the AssessmentItem being edited. It may not be set
|
|
@@ -121,10 +122,10 @@ class ItemEditor extends React.Component<Props> {
|
|
|
121
122
|
// eslint-disable-next-line react/no-string-refs
|
|
122
123
|
ref="frame"
|
|
123
124
|
key={this.props.deviceType}
|
|
124
|
-
content={this.props.frameSource}
|
|
125
125
|
datasetKey="mobile"
|
|
126
126
|
datasetValue={isMobile}
|
|
127
127
|
seamless={true}
|
|
128
|
+
url={this.props.previewURL}
|
|
128
129
|
/>
|
|
129
130
|
</DeviceFramer>
|
|
130
131
|
<div
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MultiItems,
|
|
15
15
|
} from "@khanacademy/perseus";
|
|
16
16
|
import {StyleSheet, css} from "aphrodite";
|
|
17
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
17
18
|
import lens from "hubble";
|
|
18
19
|
import * as React from "react";
|
|
19
20
|
import ReactDOM from "react-dom";
|