Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/doenetml/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { DoenetViewer, DoenetEditor } from "./doenetml";
export type { DoenetMLFlags } from "./doenetml";

export {
mathjaxConfig,
Expand Down
173 changes: 173 additions & 0 deletions packages/standalone/COORDINATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Cross-Iframe Coordination

When loading multiple DoenetML documents in separate iframes, the parent window can coordinate initialization to prevent performance issues caused by simultaneous rendering.

## Quick Start

### Parent Page (with iframes)

```html
<!doctype html>
<html>
<head>
<script src="doenet-standalone.js"></script>
</head>
<body>
<h1>DoenetML Documents</h1>

<script>
// Initialize parent coordinator to serialize iframe initialization
initializeDoenetParentCoordinator({
strategy: "viewport-first",
timeoutMs: 30000
});
</script>

<iframe src="doc1.html"></iframe>
<iframe src="doc2.html"></iframe>
<iframe src="doc3.html"></iframe>
</body>
</html>
```

### Child Page (each iframe)

```html
<!doctype html>
<html>
<head>
<script src="doenet-standalone.js"></script>
</head>
<body>
<div class="doenetml-viewer" data-doenet-enable-parent-coordination="true">
<script type="text/doenetml">
<p>My DoenetML content</p>
<graph>
<point>(2,3)</point>
</graph>
</script>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.doenetml-viewer').forEach((container) => {
renderDoenetViewerToContainer(container);
});
});
</script>
</body>
</html>
```

## Strategies

### dom-order (default)

Initializes iframes in DOM order (1st iframe first, then 2nd, then 3rd, etc.).

```javascript
initializeDoenetParentCoordinator({
strategy: "dom-order"
});
```

### viewport-first

Prioritizes visible iframes, initializing those in viewport first (sorted by DOM order), then initializes remaining iframes in DOM order.

```javascript
initializeDoenetParentCoordinator({
strategy: "viewport-first"
});
```

Perfect for pages where users may scroll to see content - visible content initializes first for a responsive feel.

**Note**: Visibility is detected using an IntersectionObserver with a rootMargin
(default: 600px). Iframes are considered "visible" when they're within that margin
of the viewport edges. This is configurable via `visibilityRootMargin`.

## Script Placement

The coordinator should be initialized before or immediately after creating iframes.
This ensures the parent is listening for `DOENET_REGISTER` messages when child frames load.

## How It Works

1. **Child Registration**: When a child iframe with `data-doenet-enable-parent-coordination="true"` loads, it registers with the parent after `registrationDelayMs` (default: 100ms). Visibility changes are reported separately.

2. **Initial Wait**: The parent waits `initialWaitMs` (default: 300ms) to collect registrations from all iframes. This ensures:
- All DOM positions are captured
- Selection logic can make informed decisions

3. **Selective Permission**: The parent grants initialization permission to one iframe at a time based on strategy:
- **dom-order**: Next iframe in DOM order
- **viewport-first**: Visible iframe (by DOM order), or if none visible, next in DOM order

4. **Serialized Rendering**: Each iframe waits for permission before rendering. Only one iframe renders at a time.

5. **Continued Selection**: When an iframe completes, the parent selects and grants permission to the next iframe.

## Configuration Options

```javascript
// Parent coordinator options
initializeDoenetParentCoordinator({
// Initialization strategy (default: "dom-order")
strategy: "dom-order" | "viewport-first",

// Maximum time to wait for iframe to complete initialization (default: 30000ms)
// If exceeded, automatically proceeds to next iframe
timeoutMs: 30000,

// Time to wait for all iframes to register before granting (default: 300ms)
// Should be substantially larger (2-3x) than child registrationDelayMs
initialWaitMs: 300
});

// Child registration options (per iframe)
// The source argument is optional; when omitted/undefined the DoenetML is read
// from a <script type="text/doenetml"> child in the container.
renderDoenetViewerToContainer(container, source, {
// Enable coordination (default: false)
enableParentCoordination: true,

// Delay before registering with parent (default: 100ms)
// Must be substantially smaller than parent's initialWaitMs
registrationDelayMs: 150,

// IntersectionObserver rootMargin for visibility detection (default: "600px")
// Larger values treat near-viewport iframes as visible sooner
visibilityRootMargin: "400px",

// ...other DoenetViewer options
});
```

These options can also be set via `data-doenet` attributes on the iframe child container:

```html
<div
class="doenetml-viewer"
data-doenet-enable-parent-coordination="true"
data-doenet-registration-delay-ms="150"
data-doenet-visibility-root-margin="400px"
></div>
```

## Edge Cases

- **Unresponsive Iframe**: If an iframe fails to complete initialization, the parent waits up to `timeoutMs` before proceeding to the next iframe.
- **Rapid Registration**: All registrations during the initial wait window are captured and processed fairly.
- **Late Registrations**: Iframes that register after the initial wait window will be queued and processed after currently-active iframe completes.
- **Visibility Changes**: When using viewport-first, visibility changes after registration are tracked and can cause re-prioritization of queued iframes.

## Backward Compatibility

- Pages without a coordinator work as before (immediate initialization)
- The `enableParentCoordination` flag is optional and defaults to false
- Pages with coordinator work whether or not child pages enable the flag; unflagged pages just initialize immediately

## Console Logging

No debug logs are emitted by default.
129 changes: 97 additions & 32 deletions packages/standalone/README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,121 @@
# Standalone DoenetML Renderer

This workspace contains a standalone DoenetML renderer.
This workspace contains a standalone DoenetML renderer that can coordinate serialized initialization across multiple iframes to prevent performance issues.

## Usage
## Quick Start

Include
### Single Page

```html
<script type="module" src="doenet-standalone.js"></script>
<!doctype html>
<html>
<head>
<script src="doenet-standalone.js"></script>
</head>
<body>
<div class="doenetml-viewer">
<script type="text/doenetml">
<p>Hello DoenetML!</p>
<graph>
<point>(2,3)</point>
</graph>
</script>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.doenetml-viewer').forEach((container) => {
renderDoenetViewerToContainer(container);
});
});
</script>
</body>
</html>
```

in your webpage. Then you can call the globally-exported function `renderDoenetToContainer`, which expects
a `<div>` element containing a `<source type="text/doenetml"></source>` as a child.

For example
### Multiple Documents in Separate Iframes

**Parent page:**
```html
<script type="module">
renderDoenetToContainer(document.querySelector(".doenetml-applet"));
<script src="doenet-standalone.js"></script>
<script>
initializeDoenetParentCoordinator({
strategy: "viewport-first",
timeoutMs: 30000
});
</script>
<iframe src="doc1.html"></iframe>
<iframe src="doc2.html"></iframe>
```

<div class="doenetml-applet">
**Each iframe (doc1.html, doc2.html, etc.):**
```html
<script src="doenet-standalone.js"></script>
Comment thread
dqnykamp marked this conversation as resolved.
<div class="doenetml-viewer" data-doenet-enable-parent-coordination="true">
<script type="text/doenetml">
<p>Use this to test DoenetML</p>
<graph showNavigation="false">

<line through="(-8,8) (9,6)" />
<line through="(0,4)" slope="1/2" styleNumber="2" />

<line equation="y=2x-8" styleNumber="3" />
<line equation="x=-6" styleNumber="4" />

</graph>
<p>DoenetML content</p>
</script>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.doenetml-viewer').forEach((container) => {
renderDoenetViewerToContainer(container);
});
});
</script>
```

To pass attributes to the DoenetML react component, you may write them in kebob-case prefixed with `data-doenet`.
For example,
## API Reference

### `renderDoenetViewerToContainer(container, doenetMLSource?, options?)`

Renders a DoenetML viewer to a specific container element.

**Parameters:**
- `container`: DOM element to render into
- `doenetMLSource`: (optional) DoenetML source code. If omitted, reads from `<script type="text/doenetml">` child
- `options`: (optional) Configuration object:
- `enableParentCoordination`: Enable serialized initialization with parent coordinator (default: `false`)
- Other DoenetViewer options (flags, callbacks, etc.)

### `initializeDoenetParentCoordinator(options?)`

Coordinates initialization across multiple child iframes. Call this from the parent page that contains DoenetML iframes. Ensures only one iframe initializes at a time.

**Parameters:**
- `options`: (optional) Configuration object:
- `strategy`: `"dom-order"` (default) or `"viewport-first"`
- `timeoutMs`: Maximum wait time for iframe initialization (default: `30000`)

### `renderDoenetEditorToContainer(container, doenetMLSource?, config?)`

Renders a DoenetML editor to a container element.

**Parameters:**
- `container`: DOM element to render into
- `doenetMLSource`: (optional) DoenetML source code. If omitted, reads from `<script type="text/doenetml">` child
- `config`: (optional) Configuration object for DoenetEditor

## Configuration

Use data attributes on viewer containers to enable parent coordination:

```html
<div class="doenetml-applet">
<script type="text/doenetml" data-doenet-read-only="true">
<graph showNavigation="false">
<line equation="x=-6" styleNumber="4" />
</graph>
</script>
<div class="doenetml-viewer"
data-doenet-enable-parent-coordination="true">
<script type="text/doenetml">...</script>
</div>
```

See [COORDINATION.md](./COORDINATION.md) for detailed documentation on cross-iframe coordination.

### Coordination Strategies

These are configured on the parent coordinator via `initializeDoenetParentCoordinator()`:

- **`dom-order`** (default): Initialize iframes in DOM order
- **`viewport-first`**: Prioritize visible iframes, then initialize remaining in DOM order

## Development

Run
Expand All @@ -57,6 +124,4 @@ Run
npm run dev
```

to start a `vite` dev server that serves the test viewer and navigate to the indicated URL. By default
`index.html` is served. You can instead navigate to `index-inline-worker.html` to view the same page but
with the inlined version of the DoenetML web worker.
to start a `vite` dev server that serves the test viewer and navigate to the indicated URL.
36 changes: 36 additions & 0 deletions packages/standalone/iframe-child.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DoenetML Document</title>
<style>
body {
margin: 10px;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div class="doenetml-viewer" data-doenet-enable-parent-coordination="true">
<script type="text/doenetml">
<p>This is a DoenetML document.</p>
<graph>
<point>(2,3)</point>
<line through="(0,0) (1,1)" />
</graph>
</script>
</div>
<script type="module">
import { renderDoenetViewerToContainer } from '/src/index.tsx';
document.addEventListener('DOMContentLoaded', () => {
document
.querySelectorAll(".doenetml-viewer")
.forEach((container) => {
renderDoenetViewerToContainer(container);
});
});
</script>
</body>
</html>
Loading
Loading