Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
137 changes: 137 additions & 0 deletions packages/standalone/COORDINATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# 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 600px rootMargin, meaning iframes are considered "visible" when they're within 600px of the viewport edges.

## 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. Visibility changes are reported separately.

2. **Initial Wait**: The parent waits 100ms to collect registrations from all iframes. This ensures:
- All DOM positions are captured
- Selection logic can make informed decisions
Comment thread
dqnykamp marked this conversation as resolved.
Outdated

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
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
});
```

## 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 100ms are captured and processed fairly.
- **Late Registrations**: Iframes that register after the initial 100ms window will be queued and processed after currently-active iframe completes.
Comment thread
dqnykamp marked this conversation as resolved.
Outdated
- **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