diff --git a/.changeset/hungry-seals-refuse.md b/.changeset/hungry-seals-refuse.md
new file mode 100644
index 0000000000..b2a80347c4
--- /dev/null
+++ b/.changeset/hungry-seals-refuse.md
@@ -0,0 +1,7 @@
+---
+"@khanacademy/perseus-dev-ui": patch
+"@khanacademy/perseus": patch
+"@khanacademy/perseus-editor": patch
+---
+
+Migrate Perseus preview to use non-iframe approach
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index 8b04d3a37d..2a794db981 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -1,7 +1,7 @@
{
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
- packageAliases[pkg.name] = join(dirname(packageJsonPath), pkg.source);
+
+ // "exports" is the more modern way to declare package exports. Some,
+ // but not all, Perseus packages delcare "exports".
+ if ("exports" in pkg) {
+ // Not all packages export strings, but for those that do we need
+ // to set up an alias so Vite properly resolves them.
+ // Eg `import {strings, mockStrings} from "@khanacademy/perseus/strings";`
+ // And MOST IMPORTANTLY, this alias _must_ precede the main
+ // import, otherwise Vite will just use the main export and tack
+ // `/strings` onto the end, resulting in a path like this:
+ // `packages/perseus/src/index.ts/strings`
+ const stringsSource = pkg.exports["./strings"]?.source;
+ if (stringsSource != null) {
+ packageAliases[`${pkg.name}/strings`] = join(
+ dirname(packageJsonPath),
+ stringsSource,
+ );
+ }
+
+ const mainSource = pkg.exports["."]?.source;
+ if (mainSource == null) {
+ throw new Error(
+ `Package declares 'exports', but not provide a main export (exports["."])`,
+ );
+ }
+ packageAliases[pkg.name] = join(
+ dirname(packageJsonPath),
+ mainSource,
+ );
+ } else {
+ packageAliases[pkg.name] = join(
+ dirname(packageJsonPath),
+ pkg.source,
+ );
+ }
},
);
diff --git a/packages/perseus-editor/src/__stories__/article-editor.stories.tsx b/packages/perseus-editor/src/__stories__/article-editor.stories.tsx
index 75dabe601e..4bd239dc5c 100644
--- a/packages/perseus-editor/src/__stories__/article-editor.stories.tsx
+++ b/packages/perseus-editor/src/__stories__/article-editor.stories.tsx
@@ -5,14 +5,602 @@ import {useRef, useState} from "react";
import ArticleEditor from "../article-editor";
import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing";
+import type {PerseusRenderer} from "@khanacademy/perseus";
+
registerAllWidgetsAndEditorsForTesting();
export default {
title: "PerseusEditor/ArticleEditor",
};
+const sampleArticle: ReadonlyArray = [
+ {
+ content:
+ "The word \"radiation\" sometimes gets a bad rap. People often associate radiation with something dangerous or scary, without really knowing what it is. In reality, we're surrounded by radiation all the time. \n\n# Heading\n\n**Radiation** is energy that travels through space (not just \"outer space\"—any space). Radiation can also interact with matter. How radiation interacts with matter depends on the type of radiation and the type of matter.\n\nRadiation comes in many forms, one of which is *electromagnetic radiation*. Without electromagnetic radiation life on Earth would not be possible, nor would most modern technologies.\n\n[[☃ image 13]]\n\nLet's take a closer look at this important and fascinating type of radiation.\n\n##Electromagnetic radiation\n\nAs the name suggests, **electromagnetic (EM) radiation** is energy transferred by *electromagnetic fields* oscillating through space.\n\nEM radiation is strange—it has both wave and particle properties. Let's take a look at both.\n\n###Electromagnetic waves\n\nAn animated model of an EM wave is shown below.\n[[☃ image 1]]\nThe electric field $(\\vec{\\textbf{E}})$ is shown in $\\color{blue}\\textbf{blue}$, and the magnetic field $(\\vec{\\textbf{B}})$ is shown in $\\color{red}\\textbf{red}$. They're perpendicular to each other.\n\nA changing electric field creates a magnetic field, and a changing magnetic field creates an electric field. So, once the EM wave is generated it propagates itself through space!\n\nAs with any wave, EM waves have wavelength, frequency, and speed. The wave model of EM radiation works best on large scales. But what about the atomic scale?\n\n###Photons\n\nAt the quantum level, EM radiation exists as particles. A particle of EM radiation is called a **photon**.\n\nWe can think of photons as wave *packets*—tiny bundles of EM radiation containing specific amounts of energy. Photons are visually represented using the following symbol.\n\n[[☃ image 3]]\n\nAll EM radiation, whether modeled as waves or photons, travels at the **speed of light** $\\textbf{(c)}$ in a vacuum: \n\n$\\text{c}=3\\times10^8\\space\\pu{m/s}=300{,}000{,}000\\space\\pu{m/s}$\n\nBut, EM radiation travels at a slower speed in matter, such as water or glass.",
+ images: {},
+ widgets: {
+ "image 13": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [600, 254],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/358a87c20ab6ee70447f5fcb547010f69986828e.jpg",
+ width: 600,
+ height: 254,
+ },
+ labels: [],
+ alt: "From space, the sun appears over Earth's horizon, illuminating the atmosphere as a blue layer above Earth. Above the atmosphere, space appears black.",
+ caption:
+ "*Sunrise photo from the International Space Station. Earth's atmosphere scatters electromagnetic radiation from the sun, producing a bright sky during the day.*",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "image 1": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [627, 522],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/8100369eaf3b581d4e7bfc9f1062625309def486.gif",
+ width: 627,
+ height: 522,
+ },
+ labels: [],
+ alt: "An animation shows a blue electric field arrow oscillating up and down. Connected to the base of the electric field arrow is a magnetic field arrow, which oscillates from side to side. The two fields oscillate in unison: when one extends the other extends too, creating a repeating wave pattern.",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "image 3": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [350, 130],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/74edeeb6c6605a4e854e3a3e9db69c01dcf5508f.svg",
+ width: 350,
+ height: 130,
+ },
+ labels: [],
+ alt: "A squiggly curve drawn from left to right. The right end of the curve has an arrow point. The curve begins with a small amount of wiggle on the left, which grows in amplitude in the middle and then decreases again on the right. The result is a small wave packet.",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ },
+ },
+ {
+ content:
+ "##The electromagnetic spectrum\n\nThe wavelength, frequency, and energy of EM radiation can fall within a wide range. We call that range the **electromagnetic (EM) spectrum**. \n\nA diagram of the EM spectrum is shown below.\n\n[[☃ image 3]]\n\nLet's analyze one piece of this diagram at a time.\n\n###Wavelength\n\nWavelength is the distance from one peak of a wave to the next peak. Notice how the red wave at the top of the diagram shows the wavelength *decreasing* from left to right—the peaks get closer together. \n\nThe numbers on the diagram tell us that EM wavelength ranges from $10^3\\space(1{,}000)$ meters to $10^{-12}\\space(0.00000000001)$ meters. \n\nSome EM waves have wavelengths longer than buildings while others are shorter than atoms. That's a big difference!\n\n###Frequency\n\nFrequency is the number of wave cycles in a period of time. We can't \"see\" frequency in a stationary diagram, since frequency involves change over time.\n\nBut, we can visualize frequency with a simple example. Imagine you're creating water waves by dripping water into a pool. The more frequent the drips, the higher the frequency of waves which will spread out.\n\n[[☃ image 6]]\n\nIf you release one drip per second, you'll create a wave with a frequency of $1$ hertz $(\\pu{Hz})$. $(1\\space\\pu{Hz}=1\\space\\text{cycle}/\\text{second})$ If you release three drips per second, you'll create a $3$ hertz wave.\n\nThe numbers on the diagram tell us that EM frequency ranges from $10^4\\space(10{,}000)$ hertz to $10^{20}\\space(100{,}000{,}000{,}000{,}000{,}000{,}000)$ hertz.\n\nImagine that many water drips every second!\n\n###Energy\n\nFinally, notice on the bottom of the EM spectrum diagram that **photon energy increases as frequency increases**. \n\nThe diagram does not show numerical values for energy, but the energy of EM radiation can be measured in various units.\n\nOne unit used to measure the energy of individual photons is the electronvolt $(\\pu{eV})$.\n\nTo summarize for EM waves:\n\nshorter wavelength $\\rightarrow$ higher frequency $\\rightarrow$ higher energy photons",
+ images: {},
+ widgets: {
+ "image 3": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [650, 322],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/c91bbc26edeca8d28c474c22304ebff48ec4de52.svg",
+ width: 650,
+ height: 322,
+ },
+ labels: [],
+ alt: "A diagram of the EM spectrum shows the following regions in order from lowest frequency and energy to highest frequency and energy. Each is accompanied by an object for comparison to its wavelength.\nRadio waves: wavelength on the scale of buildings\nMicrowaves: wavelength on the scale of humans to butterflies\nInfrared: wavelength on the scale of a needle point\nVisible light: wavelength on the scale of protozoans\nUltraviolet: wavelength on the scale of molecules\nX-rays: wavelength on the scale of atoms\nGamma rays: wavelength on the scale of nuclei",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "image 6": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [400, 267],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/ad740ef4cd0a31eb3e4e7e98a66cc907834a1eca.jpg",
+ width: 400,
+ height: 267,
+ },
+ labels: [],
+ alt: "Water drips fall on a surface of water, creating circular waves that spread outward",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ },
+ },
+ {
+ content:
+ "##Let's take a tour!\n\nEach region of the EM spectrum has it's own name. Let's explore each region in more detail.\n\n###Radio waves\n\nRadio waves are EM waves with the longest wavelength and lowest frequency. AM and FM radio signals are sent using radio waves.\n\n###Microwaves\n\nMicrowaves have higher frequency than radio waves. Microwaves are produced inside microwave ovens to warm food. Most WiFi signals are also in the microwave range.\n\n###Infrared\n\nInfrared radiation has higher frequency than microwaves. Objects at \"everyday\" temperatures we interact with regularly, including our bodies, radiate infrared waves. So, we commonly associate infrared radiation with warmth.\n\nInfrared cameras show the amount of infrared radiating from an area. For example, this infrared image of Rusty the dog shows that the greatest amount of infrared radiation escapes from his eyes, mouth, and ears. His fur traps much of the energy on the rest of his body, keeping him warm.\n\n[[☃ image 1]]\n\nInfrared waves also radiate from surfaces warmed by the sun, like soil and concrete. Some of that infrared radiation escapes into space, and some is trapped by Earth's atmosphere.\n\n###Visible light\n\nThe next highest frequency band is visible light. This band of EM radiation is called \"visible\" because it's what human eyes can see. \n\nSighted people can only see when visible light enters their eyes. Nothing is visible in a room without visible light.\n\nOur brains perceive different frequencies in the visible spectrum as different colors. We see the longest wavelength, lowest frequency visible light as $\\color{red}{\\textbf{red}}$, and the shortest wavelength, highest frequency visible light as $\\color{darkviolet}{\\textbf{violet}}$.\n\nAll the other colors of the \n$\\textcolor{red}{\\textbf{r}}\\textcolor{darkorange}{\\textbf{a}}\\textcolor{gold}{\\textbf{i}}\\textcolor{green}{\\textbf{n}}\\textcolor{blue}{\\textbf{b}}\\textcolor{indigo}{\\textbf{o}}\\textcolor{darkviolet}{\\textbf{w}}$ are associated with frequencies between red and violet. Other colors, like white, result from mixtures of these frequencies entering our eyes at the same time.\n\n[[☃ image 9]]\n\n[[☃ explanation 1]]\n\n###Ultraviolet (UV)\n\nAs EM frequency increases beyond violet light, we lose the ability to see it with our eyes. We've entered the ultraviolet—or UV—range.\n\nRemember that photon energy increases with frequency. The energy of UV photons is high enough that they can cause damage to living tissues.\n\nIn addition to visible light, UV radiation enters Earth's atmosphere from the sun. The ozone layer blocks much of it, but some UV makes it to Earth's surface.\n\nSo when you're outside in the sun, make sure that you use UV protection to avoid sunburns and eye damage.\n\n###X-rays\n\nAs EM frequency increases beyond UV, we arrive at X-rays.\n\nThe energy of X-ray photons is high enough to make it **ionizing** radiation. This means that if an X-ray photon enters matter, it can knock electrons out of (ionize) atoms in the matter.\n\nThis is dangerous for organisms, since repeated ionizations in cells can lead to damage and mutations.\n\nHowever, the small dose of X-ray radiation received during a medical scan is safe. If you've ever had an X-ray image taken of your body, X-ray photons were sent into you. Dense tissues like bone block more X-rays than soft tissues, creating a shadow image when the X-rays reach a detector on the other side.\n\nCan you tell what's wrong with this person's arm from the X-ray image? Ouch.\n\n[[☃ image 4]]\n\n###Gamma rays\n\nFinally, we reach gamma rays. Gamma $(\\gamma)$ radiation has the highest frequency and energy of any EM radiation. Like X-rays, gamma rays are **ionizing** radiation. They're the most hazardous to living things.\n\nGamma rays are produced by high energy interactions in space. Gamma rays are also produced by unstable nuclei during a type of radioactive decay called *gamma decay*.\n\n[[☃ image 7]]\n\nThough gamma rays are hazardous, they can also be used for good. For example, gamma rays are used in radiation therapy to kill cancer cells.",
+ images: {},
+ widgets: {
+ "image 1": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [416, 212],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/386d98878f310b2c47b6ff9efa03bfb59d5a5448.jpg",
+ width: 416,
+ height: 212,
+ },
+ labels: [],
+ alt: "An infrared image of a dog's face, represented in visible colors. Yellow regions emit more infrared, and purple regions emit less infrared. In the infrared image, the dog's open mouth, eyes, and ears are yellow, and the rest of his face is purple.",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "image 9": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [400, 314],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/84ab83cd86b18c94388b8247e853536d3230239e.jpg",
+ width: 400,
+ height: 314,
+ },
+ labels: [],
+ alt: "A triangular glass prism spreads out different wavelengths of visible light. A beam of white light enters the prism on one side, and a rainbow of colors exits the other side.",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "explanation 1": {
+ type: "explanation",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ showPrompt: 'Does "light" only refer to visible light?',
+ hidePrompt: "Hide explanation",
+ explanation:
+ 'It depends. Sometimes scientists use "light" to refer specifically to visible light. Other times they use "light" to refer to the entire EM spectrum.\n\nSo, you can do either, as long as you\'re clear about what you\'re referring to.',
+ widgets: {},
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "image 4": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [200, 446],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/ed08bb252bcc442e308e7a4de96ff1a312727065.jpg",
+ width: 200,
+ height: 446,
+ },
+ labels: [],
+ alt: "A black and white X-ray image of a person's arm shows the arm bone in white, and the surrounding tissues in lighter gray. The bone is fractured near the elbow.",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "image 7": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [275, 187],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/e1dea0b94f32064bbb667aa882460092020afab5.png",
+ width: 275,
+ height: 187,
+ },
+ labels: [],
+ alt: "A large nucleus is shown as a clump of red protons and blue neutrons. A gamma photon, shown as a squiggly wave packet, shoots out of the nucleus.",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ },
+ },
+ {
+ content:
+ "##Summary\n\nElectromagnetic radiation consists of oscillating electric and magnetic fields. It can be modeled both as waves and as particles called *photons*. All EM radiation, from radio to gamma, travels at speed $\\text{c}$ in a vacuum. The higher the frequency of EM radiation, the higher the energy of the photons.\n\nHuman eyes can only see the visible light portion of the spectrum directly. However, we can use various detectors to make the other regions of the EM spectrum visible to us.\n\nLet's conclude by viewing the Crab Nebula in different wavelengths. A whole new universe is revealed when we view the cosmos across the electromagnetic spectrum!\n\n[[☃ image 1]]",
+ images: {},
+ widgets: {
+ "image 1": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [650, 150],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/fa3133d6d23c0852e1a81a54d78946910523e365.png",
+ width: 650,
+ height: 150,
+ },
+ labels: [],
+ alt: "The Crab Nebula appears different in the following wavelengths of the EM spectrum:\nRadio, infrared, and ultraviolet: the nebula is most intense in the middle and weaker around the outside.\nVisible light: the nebula shows several thin, fibrous features stretching toward the outside\nX-rays: a rotating spiral is visible in the middle of the nebula, with two jets shooting out\nGamma: a ball glows in the middle of the nebula",
+ caption:
+ "We can use detectors sensitive to different frequencies to produce images of the same object in different regions of the EM spectrum. The images are presented using visible light so we can see them.",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ },
+ },
+ {
+ content:
+ "##Try it!\n\n[[☃ graded-group 1]]\n\n[[☃ graded-group 2]]\n\n[[☃ graded-group 3]]\n\n[[☃ explanation 1]]\n",
+ images: {},
+ widgets: {
+ "graded-group 1": {
+ type: "graded-group",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ title: "",
+ content:
+ "\n**Problem 1**\n\nSelect the correct description for each point marked on the rainbow.\n\n[[☃ label-image 1]]",
+ images: {},
+ widgets: {
+ "label-image 1": {
+ type: "label-image",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ choices: [
+ "long wavelength & low frequency",
+ "short wavelength & high frequency",
+ ],
+ imageAlt:
+ "Section of a rainbow in the sky. From left to right, the colors appear as violet, indigo, blue, green, yellow, orange, and red.",
+ imageUrl:
+ "https://cdn.kastatic.org/ka-content-images/227d402cb09ebc1b67f197467212fa4ab3ced5b3.jpg",
+ imageWidth: 400,
+ imageHeight: 289,
+ markers: [
+ {
+ answers: [
+ "short wavelength & high frequency",
+ ],
+ label: "Violet side of the rainbow",
+ x: 35.8,
+ y: 13,
+ },
+ {
+ answers: [
+ "long wavelength & low frequency",
+ ],
+ label: "Red side of the rainbow",
+ x: 71.8,
+ y: 50.5,
+ },
+ ],
+ multipleAnswers: false,
+ hideChoicesFromInstructions: true,
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ },
+ hint: {
+ content:
+ "We can use the electromagnetic spectrum to determine the properties of each end of the rainbow.\n\n[[☃ image 1]]\n\nThe spectrum shows that for visible light, red light has the longest wavelength and lowest frequency, and violet light has the shortest wavelength and highest frequency.",
+ images: {},
+ widgets: {
+ "image 1": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [650, 322],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/c91bbc26edeca8d28c474c22304ebff48ec4de52.svg",
+ width: 650,
+ height: 322,
+ },
+ labels: [],
+ alt: "",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ },
+ },
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "graded-group 2": {
+ type: "graded-group",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ title: "",
+ content:
+ "**Problem 2**\n\nThe image below shows laser beams of different colors.\n\n[[☃ image 1]]\n\n**Which laser beam contains the highest energy photons?**\n\n[[☃ radio 1]]",
+ images: {},
+ widgets: {
+ "image 1": {
+ type: "image",
+ alignment: "block",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ title: "",
+ range: [
+ [0, 10],
+ [0, 10],
+ ],
+ box: [400, 300],
+ backgroundImage: {
+ url: "https://cdn.kastatic.org/ka-content-images/43a778bc143b510658c4be3cc447effe3d0018bf.jpg",
+ width: 400,
+ height: 300,
+ },
+ labels: [],
+ alt: "Several laser beams of different colors: red, green, blue, and violet",
+ caption: "",
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "radio 1": {
+ type: "radio",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ choices: [
+ {
+ content: "red",
+ correct: false,
+ clue: "Red photons have the *lowest* frequency and energy on the visible spectrum.",
+ },
+ {
+ content: "green",
+ correct: false,
+ clue: "Green photons have *lower* frequency and energy than blue photons on the visible spectrum.",
+ },
+ {
+ isNoneOfTheAbove: false,
+ content: "blue",
+ correct: false,
+ clue: "Blue photons have *lower* frequency and energy than violet photons on the visible spectrum.",
+ },
+ {
+ isNoneOfTheAbove: false,
+ content: "violet",
+ correct: true,
+ clue: "Violet photons have the highest frequency and energy on the visible spectrum.",
+ },
+ ],
+ randomize: false,
+ multipleSelect: false,
+ countChoices: false,
+ displayCount: null,
+ hasNoneOfTheAbove: false,
+ deselectEnabled: false,
+ },
+ version: {
+ major: 1,
+ minor: 0,
+ },
+ },
+ },
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "graded-group 3": {
+ type: "graded-group",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ title: "",
+ content:
+ "\n**Problem 3**\n\nWhich of these forms of EM radiation are **ionizing**?\n\n[[☃ radio 1]]",
+ images: {},
+ widgets: {
+ "radio 1": {
+ type: "radio",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ choices: [
+ {
+ content: "Radio waves",
+ clue: "Radio photons are *not* high enough energy to be ionizing.",
+ },
+ {
+ content: "Infrared",
+ clue: "Infrared photons are *not* high enough energy to be ionizing.",
+ },
+ {
+ isNoneOfTheAbove: false,
+ content: "X-rays",
+ clue: "X-ray photons are high enough energy to be ionizing.",
+ correct: true,
+ },
+ {
+ isNoneOfTheAbove: false,
+ content: "Gamma rays",
+ clue: "Gamma photons are high enough energy to be ionizing.",
+ correct: true,
+ },
+ ],
+ randomize: false,
+ multipleSelect: true,
+ countChoices: false,
+ displayCount: null,
+ hasNoneOfTheAbove: false,
+ deselectEnabled: false,
+ },
+ version: {
+ major: 1,
+ minor: 0,
+ },
+ },
+ },
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ "explanation 1": {
+ type: "explanation",
+ alignment: "default",
+ static: false,
+ graded: true,
+ options: {
+ static: false,
+ showPrompt: "Image credits",
+ hidePrompt: "Hide image credits",
+ explanation:
+ '"[An orbital sunrise above Shenzhen, China](https://www.nasa.gov/image-feature/an-orbital-sunrise-above-shenzhen-china)" by NASA, [Public domain](https://creativecommons.org/publicdomain/mark/1.0/)\n\n"[EM wave gif](https://commons.wikimedia.org/wiki/File:EM-Wave.gif)" by And1mu, [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)\n\n"[Photon arrow](https://commons.wikimedia.org/wiki/File:Photon_arrow.svg)" by Napy1kenobi, [Public domain](https://creativecommons.org/publicdomain/zero/1.0/deed.en)\n\n"[EM spectrum properties](https://commons.wikimedia.org/wiki/File:EM_Spectrum_Properties_edit.svg)" by Inductiveload, [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/deed.en)\n\n"[Frozen](https://commons.wikimedia.org/wiki/File:Frozen_(86941053).jpeg)" by Ricardo Costa, [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/deed.en)\n\n"[Infrared dog](https://commons.wikimedia.org/wiki/File:Infrared_dog.jpg)" by NASA/IPAC, [Public domain](https://creativecommons.org/publicdomain/mark/1.0/)\n\n"[Prism flat rainbow](https://commons.wikimedia.org/wiki/File:Prism_flat_rainbow.jpg)" by Kelvinsong, [Public domain](https://creativecommons.org/publicdomain/mark/1.0/)\n\n"[Upper arm fracture from arm wrestling](https://commons.wikimedia.org/wiki/File:Oberarmfraktur_durch_Armdruecken_02.jpg)" by Hellerhoff, [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)\n\n"[Gamma decay](https://commons.wikimedia.org/wiki/File:Gamma_Decay01.svg)" by And1mu, [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)\n\n"[Crab Nebula in multiple wavelengths](https://commons.wikimedia.org/wiki/File:Crab_Nebula_in_Multiple_Wavelengths.png)" by Torres997, [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/deed.en)\n\n"[A rainbow viewed from Earl\'s Croome](https://commons.wikimedia.org/wiki/File:A_rainbow_viewed_from_Earl%27s_Croome_-_geograph.org.uk_-_5161205.jpg)" by Philip Halling, [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/deed.en)\n\n"[LASER](https://commons.wikimedia.org/wiki/File:LASER.jpg)" by Peng Jiajie, [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/deed.en)\n',
+ widgets: {},
+ },
+ version: {
+ major: 0,
+ minor: 0,
+ },
+ },
+ },
+ },
+];
+
export const Base = (): React.ReactElement => {
- const [state, setState] = useState();
+ const [state, setState] = useState(sampleArticle);
const articleEditorRef = useRef();
function handleChange(value) {
diff --git a/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx b/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx
index 5b358a0071..511bd18c5f 100644
--- a/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx
+++ b/packages/perseus-editor/src/__stories__/editor-page-with-storybook-preview.tsx
@@ -1,24 +1,15 @@
import {
- Renderer,
type APIOptions,
type DeviceType,
type Hint,
type PerseusAnswerArea,
type PerseusRenderer,
} from "@khanacademy/perseus";
-import Button from "@khanacademy/wonder-blocks-button";
import {View} from "@khanacademy/wonder-blocks-core";
-import IconButton from "@khanacademy/wonder-blocks-icon-button";
-import {Strut} from "@khanacademy/wonder-blocks-layout";
-import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
-import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
-import xIcon from "@phosphor-icons/core/regular/x.svg";
import {action} from "@storybook/addon-actions";
-import {StyleSheet} from "aphrodite";
import * as React from "react";
// eslint-disable-next-line import/no-relative-packages
-import {mockStrings} from "../../../perseus/src/strings";
import EditorPage from "../editor-page";
import {flags} from "./flags-for-api-options";
@@ -27,13 +18,14 @@ type Props = {
apiOptions?: APIOptions;
question?: PerseusRenderer;
hints?: ReadonlyArray;
+ developerMode?: boolean;
};
const onChangeAction = action("onChange");
function EditorPageWithStorybookPreview(props: Props) {
const [previewDevice, setPreviewDevice] =
- React.useState("phone");
+ React.useState("desktop");
const [jsonMode, setJsonMode] = React.useState(false);
const [answerArea, setAnswerArea] = React.useState<
PerseusAnswerArea | undefined | null
@@ -45,8 +37,6 @@ function EditorPageWithStorybookPreview(props: Props) {
props.hints,
);
- const [panelOpen, setPanelOpen] = React.useState(true);
-
const apiOptions = props.apiOptions ?? {
isMobile: false,
flags,
@@ -60,7 +50,7 @@ function EditorPageWithStorybookPreview(props: Props) {
onPreviewDeviceChange={(newDevice) =>
setPreviewDevice(newDevice)
}
- developerMode={true}
+ developerMode={props.developerMode}
jsonMode={jsonMode}
answerArea={answerArea}
question={question}
@@ -85,85 +75,8 @@ function EditorPageWithStorybookPreview(props: Props) {
}
}}
/>
-
- {/* Button to open panel */}
- {!panelOpen && (
-
- )}
-
- {/* Panel to show the question/hint previews */}
- {panelOpen && (
-
- {/* Close button */}
- setPanelOpen(!panelOpen)}
- style={styles.closeButton}
- />
-
-
- {/* Question preview */}
-
-
-
- {/* Hints preview */}
- {hints?.map((hint, index) => (
-
-
- {`Hint ${index + 1}`}
-
-
- ))}
-
- )}
);
}
-const styles = StyleSheet.create({
- panel: {
- position: "fixed",
- right: 0,
- height: "90vh",
- overflow: "auto",
- flex: "none",
- backgroundColor: color.fadedBlue16,
- padding: spacing.medium_16,
- borderRadius: spacing.small_12,
- alignItems: "end",
- },
- panelInner: {
- flex: "none",
- backgroundColor: color.white,
- borderRadius: spacing.xSmall_8,
- marginTop: spacing.medium_16,
- width: "100%",
- padding: spacing.xSmall_8,
- },
- closeButton: {
- margin: 0,
- },
- openPanelButton: {
- position: "fixed",
- right: spacing.medium_16,
- // Extra space so it doesn't get covered up by storybook's
- // "Style warnings" button.
- bottom: spacing.xxxLarge_64,
- },
-});
-
export default EditorPageWithStorybookPreview;
diff --git a/packages/perseus-editor/src/__stories__/editor-page.stories.tsx b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx
index 33569fa99d..76d37d68ab 100644
--- a/packages/perseus-editor/src/__stories__/editor-page.stories.tsx
+++ b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx
@@ -4,12 +4,17 @@ import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widge
import EditorPageWithStorybookPreview from "./editor-page-with-storybook-preview";
+import type {Meta, StoryObj} from "@storybook/react";
+
registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry:
-export default {
+const meta: Meta = {
title: "PerseusEditor/EditorPage",
+ component: EditorPageWithStorybookPreview,
+ args: {developerMode: false},
};
+export default meta;
-export const Demo = (): React.ReactElement => {
- return ;
-};
+type Story = StoryObj;
+
+export const Demo: Story = {};
diff --git a/packages/perseus-editor/src/__stories__/iframe-renderer.stories.tsx b/packages/perseus-editor/src/__stories__/iframe-renderer.stories.tsx
new file mode 100644
index 0000000000..d915ef485c
--- /dev/null
+++ b/packages/perseus-editor/src/__stories__/iframe-renderer.stories.tsx
@@ -0,0 +1,77 @@
+import {Renderer} from "@khanacademy/perseus";
+// eslint-disable-next-line monorepo/no-internal-import
+import {mockStrings} from "@khanacademy/perseus/strings";
+
+import IFrameRenderer from "../iframe-renderer";
+
+import type {PerseusRenderer} from "@khanacademy/perseus";
+import type {Meta, StoryObj} from "@storybook/react";
+
+const meta: Meta = {
+ title: "PerseusEditor/IFrameRenderer",
+ component: IFrameRenderer,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const question: PerseusRenderer = {
+ content:
+ "Which of the following values of $x$ satisfies the equation $\\sqrt{64}=x$ ?\n\n[[\u2603 radio 1]]\n\n",
+ images: {},
+ widgets: {
+ "radio 1": {
+ graded: true,
+ version: {
+ major: 1,
+ minor: 0,
+ },
+ static: false,
+ type: "radio",
+ options: {
+ displayCount: null,
+ onePerLine: false,
+ choices: [
+ {
+ content: "$-8$ and $8$",
+ correct: false,
+ clue: "The square root operation ($\\sqrt{\\phantom{x}}$) calculates *only* the positive square root when performed on a number, so $x$ is equal to *only* $8$.",
+ },
+ {
+ content: "$-8$",
+ correct: false,
+ clue: "While $(-8)^2=64$, the square root operation ($\\sqrt{\\phantom{x}}$) calculates *only* the positive square root when performed on a number.",
+ },
+ {
+ content: "$8$",
+ correct: true,
+ isNoneOfTheAbove: false,
+ clue: "$8$ is the positive square root of $64$.",
+ },
+ {
+ content: "No value of $x$ satisfies the equation.",
+ correct: false,
+ isNoneOfTheAbove: false,
+ clue: "$8$ satisfies the equation.",
+ },
+ ],
+ countChoices: false,
+ hasNoneOfTheAbove: false,
+ multipleSelect: false,
+ randomize: false,
+ deselectEnabled: false,
+ },
+ alignment: "default",
+ },
+ },
+};
+
+export const Primary: Story = {
+ args: {
+ style: {width: "100%", height: "500px"},
+ styleSelector: 'link, style[type="text/css"]',
+ children: (
+
+ ),
+ },
+};
diff --git a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx
index f672376960..f45f0ff4c1 100644
--- a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx
+++ b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx
@@ -177,7 +177,9 @@ export const WithSaveWarnings = (): React.ReactElement => {
segmentWithLockedFigures,
);
const [hints, setHints] = React.useState | undefined>();
- const [saveWarnings, setSaveWarnings] = React.useState([]);
+ const [saveWarnings, setSaveWarnings] = React.useState(
+ [],
+ );
const editorPageRef = React.useRef(null);
diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx
index 980be9777e..535f62e604 100644
--- a/packages/perseus-editor/src/article-editor.tsx
+++ b/packages/perseus-editor/src/article-editor.tsx
@@ -13,20 +13,23 @@ import DeviceFramer from "./components/device-framer";
import JsonEditor from "./components/json-editor";
import SectionControlButton from "./components/section-control-button";
import Editor from "./editor";
-import IframeContentRenderer from "./iframe-content-renderer";
+import ContentRenderer from "./preview/content-renderer";
-import type {APIOptions, Changeable, ImageUploader} from "@khanacademy/perseus";
+import type {
+ APIOptions,
+ Changeable,
+ ImageUploader,
+ PerseusRenderer,
+} from "@khanacademy/perseus";
const {HUD, InlineIcon} = components;
const {iconCircleArrowDown, iconCircleArrowUp, iconPlus, iconTrash} = icons;
-type RendererProps = {
- content?: string;
- widgets?: any;
- images?: any;
-};
+type JsonType =
+ | ReadonlyArray>
+ | PerseusRenderer
+ | ReadonlyArray;
-type JsonType = RendererProps | ReadonlyArray;
type DefaultProps = {
contentPaths?: ReadonlyArray;
json: JsonType;
@@ -61,62 +64,9 @@ export default class ArticleEditor extends React.Component {
highlightLint: true,
};
- componentDidMount() {
- this._updatePreviewFrames();
- }
-
- componentDidUpdate() {
- this._updatePreviewFrames();
- }
+ editorRefs: Array = [];
- _updatePreviewFrames() {
- if (this.props.mode === "preview") {
- // eslint-disable-next-line react/no-string-refs
- // @ts-expect-error - TS2339 - Property 'sendNewData' does not exist on type 'ReactInstance'.
- this.refs["frame-all"].sendNewData({
- type: "article-all",
- data: this._sections().map((section, i) => {
- return this._apiOptionsForSection(section, i);
- }),
- });
- } else if (this.props.mode === "edit") {
- this._sections().forEach((section, i) => {
- // eslint-disable-next-line react/no-string-refs
- // @ts-expect-error - TS2339 - Property 'sendNewData' does not exist on type 'ReactInstance'.
- this.refs["frame-" + i].sendNewData({
- type: "article",
- data: this._apiOptionsForSection(section, i),
- });
- });
- }
- }
-
- _apiOptionsForSection(section: RendererProps, sectionIndex: number): any {
- // eslint-disable-next-line react/no-string-refs
- const editor = this.refs[`editor${sectionIndex}`];
- return {
- apiOptions: {
- ...ApiOptions.defaults,
- ...this.props.apiOptions,
-
- // Alignment options are always available in article
- // editors
- showAlignmentOptions: true,
- isArticle: true,
- },
- json: section,
- useNewStyles: this.props.useNewStyles,
- linterContext: {
- contentType: "article",
- highlightLint: this.state.highlightLint,
- paths: this.props.contentPaths,
- },
- // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
- legacyPerseusLint: editor ? editor.getSaveWarnings() : [],
- };
- }
-
- _sections(): ReadonlyArray {
+ _sections(): ReadonlyArray {
return Array.isArray(this.props.json)
? this.props.json
: [this.props.json];
@@ -201,15 +151,19 @@ export default class ArticleEditor extends React.Component {
{
+ this.editorRefs[i] = editor;
+ }}
/>
@@ -261,22 +215,61 @@ export default class ArticleEditor extends React.Component {
}
_renderIframePreview(
- i: number | string,
+ sectionIndex: number | "all",
nochrome: boolean,
): React.ReactElement {
const isMobile =
this.props.screen === "phone" || this.props.screen === "tablet";
+ const apiOptions = {
+ ...ApiOptions.defaults,
+ ...this.props.apiOptions,
+ isMobile,
+ // Alignment options are always available in article
+ // editors
+ showAlignmentOptions: true,
+ isArticle: true,
+ };
+
+ const sections = this._sections();
return (
-
+ {sectionIndex === "all" ? (
+ sections.map((section, i) => (
+
+ ))
+ ) : (
+
+ )}
);
}
@@ -293,15 +286,14 @@ export default class ArticleEditor extends React.Component {
this.props.onChange({json: newJson});
};
- _handleEditorChange: (i: number, newProps: RendererProps) => void = (
- i,
- newProps,
- ) => {
- const sections = _.clone(this._sections());
- // @ts-expect-error - TS2542 - Index signature in type 'readonly RendererProps[]' only permits reading.
- sections[i] = _.extend({}, sections[i], newProps);
+ _handleEditorChange(
+ sectionIndex: number,
+ newProps: Partial,
+ ) {
+ const sections = [...this._sections()];
+ sections[sectionIndex] = {...sections[sectionIndex], ...newProps};
this.props.onChange({json: sections});
- };
+ }
_handleMoveSectionEarlier(i: number) {
if (i === 0) {
@@ -309,9 +301,9 @@ export default class ArticleEditor extends React.Component {
}
const sections = _.clone(this._sections());
const section = sections[i];
- // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'?
+ // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'?
sections.splice(i, 1);
- // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'?
+ // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'?
sections.splice(i - 1, 0, section);
this.props.onChange({
json: sections,
@@ -324,9 +316,9 @@ export default class ArticleEditor extends React.Component {
return;
}
const section = sections[i];
- // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'?
+ // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'?
sections.splice(i, 1);
- // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'?
+ // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'?
sections.splice(i + 1, 0, section);
this.props.onChange({
json: sections,
@@ -358,7 +350,7 @@ export default class ArticleEditor extends React.Component {
_handleRemoveSection(i: number) {
const sections = _.clone(this._sections());
- // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'?
+ // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly Renderer[]'. Did you mean 'slice'?
sections.splice(i, 1);
this.props.onChange({
json: sections,
@@ -367,10 +359,8 @@ export default class ArticleEditor extends React.Component {
serialize(): JsonType {
if (this.props.mode === "edit") {
- return this._sections().map((section, i) => {
- // eslint-disable-next-line react/no-string-refs
- // @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'.
- return this.refs["editor" + i].serialize();
+ return this._sections().map((_, i) => {
+ return this.editorRefs[i]!.serialize();
});
}
if (this.props.mode === "preview" || this.props.mode === "json") {
@@ -383,12 +373,12 @@ export default class ArticleEditor extends React.Component {
}
/**
- * Returns an array, with one element be section.
+ * Returns an array, with one element per section.
* Each element is an array of lint warnings present in that section.
*
* This function can currently only be called in edit mode.
*/
- getSaveWarnings(): ReadonlyArray {
+ getSaveWarnings(): ReadonlyArray> {
if (this.props.mode !== "edit") {
// TODO(joshuan): We should be able to get save warnings in
// preview mode.
@@ -399,9 +389,7 @@ export default class ArticleEditor extends React.Component {
}
return this._sections().map((section, i) => {
- // eslint-disable-next-line react/no-string-refs
- // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'.
- return this.refs["editor" + i].getSaveWarnings();
+ return this.editorRefs[i]?.getSaveWarnings() ?? [];
});
}
diff --git a/packages/perseus-editor/src/components/device-framer.tsx b/packages/perseus-editor/src/components/device-framer.tsx
index c654e551f0..e43b37f6a6 100644
--- a/packages/perseus-editor/src/components/device-framer.tsx
+++ b/packages/perseus-editor/src/components/device-framer.tsx
@@ -60,7 +60,7 @@ const DeviceFramer = ({
);
if (nochrome) {
- // Render content inside a variable height iframe. Used on the
+ // Render content inside a variable height div. Used on the
// "edit" table of the content editor. In this mode, PerseusFrame
// will draw the border and reserve space on the right for
// lint indicators.
diff --git a/packages/perseus-editor/src/components/viewport-resizer.tsx b/packages/perseus-editor/src/components/viewport-resizer.tsx
index 7acc3a82b3..01a6ec9b65 100644
--- a/packages/perseus-editor/src/components/viewport-resizer.tsx
+++ b/packages/perseus-editor/src/components/viewport-resizer.tsx
@@ -2,14 +2,18 @@
* A component that displays controls for choosing a viewport size.
* Renders three buttons: "Phone", "Tablet", and "Desktop".
*/
-import {components, constants, icons} from "@khanacademy/perseus";
+import {components, constants} from "@khanacademy/perseus";
+import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
+import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
+import phosphorDesktop from "@phosphor-icons/core/regular/desktop.svg";
+import phosphorPhone from "@phosphor-icons/core/regular/device-mobile.svg";
+import phosphorTablet from "@phosphor-icons/core/regular/device-tablet.svg";
import * as React from "react";
import type {DeviceType} from "@khanacademy/perseus";
-const {ButtonGroup, InlineIcon} = components;
+const {ButtonGroup} = components;
const {devices} = constants;
-const {iconDesktop, iconMobilePhone, iconTablet} = icons;
type Props = {
/** The current device type that is selected. */
@@ -24,23 +28,22 @@ type Props = {
const ViewportResizer = (props: Props) => {
const phoneButtonContents = (
- Phone
+ Phone
);
const tabletButtonContents = (
- Tablet
+ Tablet
);
const desktopButtonContents = (
- Desktop
+ Desktop
);
- // TODO(david): Allow input of custom viewport sizes.
return (
-
+
Viewport:{" "}
{
]}
onChange={props.onViewportSizeChanged}
/>
-
+
);
};
diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx
index 59b140e9f5..955fd8815f 100644
--- a/packages/perseus-editor/src/editor-page.tsx
+++ b/packages/perseus-editor/src/editor-page.tsx
@@ -1,9 +1,16 @@
import {components, ApiOptions, ClassNames} from "@khanacademy/perseus";
+import {View} from "@khanacademy/wonder-blocks-core";
+import {Checkbox} from "@khanacademy/wonder-blocks-form";
+import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
+import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
+import Tooltip from "@khanacademy/wonder-blocks-tooltip";
+import {LabelSmall} from "@khanacademy/wonder-blocks-typography";
+import warning from "@phosphor-icons/core/bold/warning-circle-bold.svg";
import * as React from "react";
+import invariant from "tiny-invariant";
import _ from "underscore";
import JsonEditor from "./components/json-editor";
-import ViewportResizer from "./components/viewport-resizer";
import CombinedHintsEditor from "./hint-editor";
import ItemEditor from "./item-editor";
@@ -62,7 +69,6 @@ type State = {
};
class EditorPage extends React.Component {
- _isMounted: boolean;
renderer: any;
itemEditor = React.createRef();
@@ -94,33 +100,6 @@ class EditorPage extends React.Component {
wasAnswered: false,
highlightLint: true,
};
-
- this._isMounted = false;
- }
-
- componentDidMount() {
- // TODO(scottgrant): This is a hack to remove the deprecated call to
- // this.isMounted() but is still considered an anti-pattern.
- this._isMounted = true;
-
- this.updateRenderer();
- }
-
- componentDidUpdate() {
- // NOTE: It is required to delay the preview update until after the
- // current frame, to allow for ItemEditor to render its widgets.
- // This then enables to serialize the widgets properties correctly,
- // in order to send data to the preview iframe (IframeContentRenderer).
- // Otherwise, widgets will render in an "empty" state in the preview.
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
- // eslint-disable-next-line no-restricted-syntax
- setTimeout(() => {
- this.updateRenderer();
- });
- }
-
- componentWillUnmount() {
- this._isMounted = false;
}
toggleJsonMode: () => void = () => {
@@ -136,50 +115,6 @@ class EditorPage extends React.Component {
);
};
- updateRenderer() {
- // Some widgets (namely the image widget) like to call onChange before
- // anything has actually been mounted, which causes problems here. We
- // just ensure don't update until we've mounted
- const hasEditor = !this.props.developerMode || !this.props.jsonMode;
- if (!this._isMounted || !hasEditor) {
- return;
- }
-
- const touch =
- this.props.previewDevice === "phone" ||
- this.props.previewDevice === "tablet";
- const deviceBasedApiOptions: APIOptionsWithDefaults = {
- ...this.getApiOptions(),
- customKeypad: touch,
- isMobile: touch,
- };
-
- this.itemEditor.current?.triggerPreviewUpdate({
- type: "question",
- data: _({
- item: this.serialize(),
- apiOptions: deviceBasedApiOptions,
- initialHintsVisible: 0,
- device: this.props.previewDevice,
- linterContext: {
- contentType: "exercise",
- highlightLint: this.state.highlightLint,
- // TODO(CP-4838): is it okay to use [] as a default?
- paths: this.props.contentPaths || [],
- },
- reviewMode: true,
- legacyPerseusLint: this.itemEditor.current?.getSaveWarnings(),
- }).extend(
- _(this.props).pick(
- "workAreaSelector",
- "solutionAreaSelector",
- "hintsAreaSelector",
- "problemNum",
- ),
- ),
- });
- }
-
getApiOptions(): APIOptionsWithDefaults {
return {
...ApiOptions.defaults,
@@ -187,19 +122,29 @@ class EditorPage extends React.Component {
};
}
- getSaveWarnings(): any {
- const issues1 = this.itemEditor.current?.getSaveWarnings();
- const issues2 = this.hintsEditor.current?.getSaveWarnings();
+ getSaveWarnings(): ReadonlyArray {
+ const issues1 = this.itemEditor.current?.getSaveWarnings() ?? [];
+ const issues2 = this.hintsEditor.current?.getSaveWarnings() ?? [];
return issues1.concat(issues2);
}
- serialize(options?: {keepDeletedWidgets?: boolean}): any | PerseusItem {
+ serialize(options?: {keepDeletedWidgets?: boolean}): PerseusItem {
if (this.props.jsonMode) {
return this.state.json;
}
- return _.extend(this.itemEditor.current?.serialize(options), {
+ invariant(this.itemEditor.current != null);
+ invariant(this.hintsEditor.current != null);
+ return {
+ ...this.itemEditor.current?.serialize(options),
hints: this.hintsEditor.current?.serialize(options),
- });
+
+ // Note(jeremy): These two are to satisfy the fact that our
+ // PerseusItem type really should be a union between a multi item
+ // and a standard perseus item (also that the `answer` field, which
+ // is deprecated, is required).
+ _multi: undefined,
+ answer: undefined,
+ };
}
handleChange: ChangeHandler = (toChange, cb, silent) => {
@@ -224,48 +169,78 @@ class EditorPage extends React.Component {
}
render(): React.ReactNode {
- let className = "framework-perseus";
+ const className = "framework-perseus";
- const touch =
- this.props.previewDevice === "phone" ||
- this.props.previewDevice === "tablet";
- const deviceBasedApiOptions: APIOptionsWithDefaults = {
- ...this.getApiOptions(),
- customKeypad: touch,
- isMobile: touch,
- };
-
- if (deviceBasedApiOptions.isMobile) {
- className += " " + ClassNames.MOBILE;
- }
+ const apiOptions = this.getApiOptions();
return (
-
- {this.props.developerMode && (
-
- {" "}
-
- )}
+
+
+ {" "}
+ {this.props.developerMode && (
+
+ )}
+
{!this.props.jsonMode && (
-
+
+
+ Note: Don't forget to check how this
+ exercise looks on a phone and tablet by using
+ the "Preview" tab.{" "}
+
+
+ This preview is designed to give
+ you fast feedback when editing
+ the exercise. To be sure it
+ looks correct on all devices a
+ Khan Academy learner may view it
+ on, please use the "Preview"
+ tab.
+
+
+ }
+ >
+
+
+
+
)}
{!this.props.jsonMode && (
+ // NOTE: This component positions itself using fixed
+ // positioning, so even though it appears here, near
+ // the JSON Mode and Viewport Resizer elements, it
+ // shows up in a completely different place on the page
+ // visually.
{
}}
/>
)}
-