@khanacademy/perseus-editor 0.1.2 → 0.3.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/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.3.0",
2
7
  "publishConfig": {
3
8
  "access": "public"
4
9
  },
5
- "name": "@khanacademy/perseus-editor",
6
- "version": "0.1.2",
7
- "description": "Perseus editors",
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.1"
24
+ "@khanacademy/perseus": "^0.2.4"
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
- "author": "",
41
- "license": "MIT"
49
+ "keywords": []
42
50
  }
@@ -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
  );
@@ -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: AphroditeStyle | Array<AphroditeStyle>,
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<AphroditeStyle>),
73
+ style: ([]: Array<CSSProperties>),
74
74
  element: "a",
75
75
  };
76
76
 
@@ -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
- frameSource={this.props.frameSource}
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
- frameSource={this.props.frameSource}
345
+ previewURL={this.props.previewURL}
343
346
  highlightLint={this.state.highlightLint}
344
347
  />
345
348
  )}
@@ -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
- const IframeContentRenderer: $FlowFixMe = createReactClass({
56
- displayName: "IframeContentRenderer",
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
- propTypes: {
59
- // The HTML content to render to the iframe
60
- content: PropTypes.string.isRequired,
59
+ // The data-* suffix for passing information to the iframe's JS
60
+ datasetKey: string,
61
61
 
62
- // The data-* suffix for passing information to the iframe's JS
63
- datasetKey: PropTypes.any,
62
+ // The value of the data-* attribute
63
+ datasetValue: $FlowFixMe,
64
64
 
65
- // The value of the data-* attribute
66
- datasetValue: PropTypes.any,
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
- // Whether to make the iframe's height match its content's height,
69
- // used to prevent scrolling inside the iframe.
70
- seamless: PropTypes.bool,
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: function () {
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: function (nextProps) {
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: function (prevProps) {
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: function () {
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: function () {
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
- this._frame = document.createElement("iframe");
136
- this._frame.style.width = "100%";
137
- this._frame.style.height = "100%";
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
- this._frame.dataset[this.props.datasetKey] =
144
- this.props.datasetValue;
144
+ frame.dataset[this.props.datasetKey] = this.props.datasetValue;
145
145
  }
146
- this._frame.dataset.id = this.iframeID;
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
- this._frame.dataset.lintGutter = "true";
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(this._frame);
181
- },
159
+ this.refs.container.appendChild(frame);
160
+
161
+ this._frame = frame;
162
+ }
182
163
 
183
- sendNewData: function (data) {
184
- if (this._isMounted && data && this._frame.contentWindow) {
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
- this._frame.contentWindow.postMessage(this.iframeID, "*");
173
+ frame.contentWindow.postMessage(this.iframeID, "*");
192
174
  }
193
- },
175
+ }
194
176
 
195
- render: function () {
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/index.js CHANGED
@@ -1,6 +1,4 @@
1
1
  // @flow
2
- export * as Perseus from "@khanacademy/perseus";
3
-
4
2
  export {default as ArticleEditor} from "./article-editor.jsx";
5
3
  export {default as DeviceFramer} from "./components/device-framer.jsx";
6
4
  export {default as ViewportResizer} from "./components/viewport-resizer.jsx";
@@ -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