Skip to content

Commit e2534e2

Browse files
authored
Rewrite inject_dartpad to expose some functionality in library (#247)
1 parent 9321cff commit e2534e2

File tree

7 files changed

+1733
-1036
lines changed

7 files changed

+1733
-1036
lines changed

pkgs/inject_dartpad/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
## DartPad injection
22

33
To embed a DartPad with arbitrary Dart in to your web page, add the
4-
JS file found at `lib/inject_dartpad.js` with a `<script>` tag,
4+
JS file found at `lib/inject_dartpad.dart.js` with a `<script>` tag,
55
set up to run after the DOM is ready.
66

77
This might look something like the following, depending on where
88
your version of the JS file is stored.
99

1010
```html
11-
<script defer src="inject_dartpad.js"></script>
11+
<script defer src="inject_dartpad.dart.js"></script>
1212
```
1313

1414
### Declare code to inject
@@ -42,10 +42,10 @@ this and other behavior, add one or more of the following options:
4242
- `data-height="<CSS height>"`
4343
To specify the initial height of the injected iframe element.
4444

45-
### Developing script
45+
### Develop injection script
4646

4747
To work on the script itself, modify the code within `/web/inject_dartpad.dart`.
4848
To compile the code to JavaScript, use the latest Dart SDK,
4949
verify you have the latest dependencies (`dart pub upgrade`), and then
5050
run `dart run tool/compile.dart`.
51-
The updated file will be written to `/lib/inject_dartpad.js`.
51+
The updated file will be written to `/lib/inject_dartpad.dart.js`.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:inject_dartpad/inject_dartpad.dart';
2+
import 'package:web/web.dart' as web;
3+
4+
void main() async {
5+
// Create the embedded DartPad instance manager.
6+
final dartPad = EmbeddedDartPad.create(
7+
iframeId: 'my-dartpad',
8+
theme: DartPadTheme.light,
9+
);
10+
11+
// Initialize the embedded DartPad.
12+
await dartPad.initialize(
13+
onElementCreated: (iframe) {
14+
// Add any extra styles or attributes to the created iframe.
15+
iframe.style.height = '560';
16+
17+
// Add the iframe to the document body.
18+
// This is necessary for the embed to load.
19+
web.document.body!.append(iframe);
20+
},
21+
);
22+
23+
// After awaiting initialization, you can update the code in the DartPad.
24+
dartPad.updateCode(r'''
25+
void main() {
26+
print('Hello, I am Dash!');
27+
}''');
28+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:js_interop';
7+
8+
import 'package:web/web.dart' as web;
9+
10+
/// An iframe-embedded DartPad that can be injected into a web page,
11+
/// then have its source code updated.
12+
///
13+
/// Example usage:
14+
///
15+
/// ```dart
16+
/// import 'package:inject_dartpad/inject_dartpad.dart';
17+
/// import 'package:web/web.dart' as web;
18+
///
19+
/// void main() async {
20+
/// final dartPad = EmbeddedDartPad.create(
21+
/// iframeId: 'my-dartpad',
22+
/// theme: DartPadTheme.light,
23+
/// );
24+
///
25+
/// await dartPad.initialize(
26+
/// onElementCreated: (iframe) {
27+
/// iframe.style.height = '560';
28+
///
29+
/// web.document.body!.append(iframe);
30+
/// },
31+
/// );
32+
///
33+
/// dartPad.updateCode('''
34+
/// void main() {
35+
/// print("Hello, I'm Dash!");
36+
/// }''');
37+
/// }
38+
/// ```
39+
final class EmbeddedDartPad {
40+
/// The unique identifier that's used to identify the created DartPad iframe.
41+
///
42+
/// This ID is used both as the HTML element `id` and
43+
/// as the iframe's `name` attribute for message targeting.
44+
final String iframeId;
45+
46+
/// The full URL of the DartPad iframe including
47+
/// all path segments and query parameters.
48+
final String _iframeUrl;
49+
50+
/// Tracks the initialization state of the embedded DartPad.
51+
///
52+
/// Completes when the DartPad iframe has loaded and
53+
/// sent a 'ready' message indicating it can receive code updates.
54+
final Completer<void> _initializedCompleter = Completer();
55+
56+
/// Creates an embedded DartPad instance with
57+
/// the specified [iframeId] and [iframeUrl].
58+
EmbeddedDartPad._({required this.iframeId, required String iframeUrl})
59+
: _iframeUrl = iframeUrl;
60+
61+
/// Creates a new embedded DartPad element with the specified configuration.
62+
///
63+
/// Once created, the DartPad must be initialized by
64+
/// calling and awaiting [initialize].
65+
///
66+
/// The [iframeId] is used to identify the created DartPad iframe.
67+
/// It must be unique within the document and a valid HTML element ID.
68+
///
69+
/// The [scheme] and [host] are used to construct the DartPad iframe URL.
70+
/// [scheme] defaults to 'https' and [host] defaults to 'dartpad.dev'.
71+
///
72+
/// To control the appearance of the embedded DartPad,
73+
/// you can switch to the [embedLayout] and choose a specific [theme].
74+
factory EmbeddedDartPad.create({
75+
required String iframeId,
76+
String? scheme,
77+
String? host,
78+
bool? embedLayout,
79+
DartPadTheme? theme = DartPadTheme.auto,
80+
}) {
81+
final dartPadUrl = Uri(
82+
scheme: scheme ?? 'https',
83+
host: host ?? 'dartpad.dev',
84+
queryParameters: <String, String>{
85+
if (embedLayout ?? true) 'embed': '$embedLayout',
86+
if (theme != DartPadTheme.auto) 'theme': '$theme',
87+
},
88+
).toString();
89+
90+
return EmbeddedDartPad._(iframeId: iframeId, iframeUrl: dartPadUrl);
91+
}
92+
93+
/// Creates and initializes the embedded DartPad iframe.
94+
///
95+
/// Must be called and awaited before interacting with this instance,
96+
/// such as updating the DartPad editor's current source code.
97+
///
98+
/// The created iframe is passed to the [onElementCreated] callback,
99+
/// which should be used to add the iframe to the document and
100+
/// further configure its attributes, such as classes and size.
101+
///
102+
/// For example, if you want to embed the DartPad in
103+
/// a container with an ID of 'dartpad-container':
104+
///
105+
/// ```dart
106+
/// await dartPad.initialize(
107+
/// onElementCreated: (iframe) {
108+
/// document.getElementById('dartpad-container')!.append(iframe);
109+
/// },
110+
/// );
111+
/// ```
112+
Future<void> initialize({
113+
required void Function(web.HTMLIFrameElement iframe) onElementCreated,
114+
}) async {
115+
if (_initialized) return;
116+
117+
// Start listening for the 'ready' message from the embedded DartPad.
118+
late final JSExportedDartFunction readyHandler;
119+
readyHandler = (web.MessageEvent event) {
120+
if (event.data case _EmbedReadyMessage(type: 'ready', :final sender?)) {
121+
// Verify the message is sent from the corresponding iframe,
122+
// in case there are multiple DartPads being embedded at the same time.
123+
if (sender != iframeId) {
124+
return;
125+
}
126+
127+
web.window.removeEventListener('message', readyHandler);
128+
if (!_initialized) {
129+
// Signal to the caller that the DartPad is ready
130+
// for Dart code to be injected.
131+
_initializedCompleter.complete();
132+
}
133+
}
134+
}.toJS;
135+
136+
web.window.addEventListener('message', readyHandler);
137+
138+
final iframe = web.HTMLIFrameElement()
139+
..src = _iframeUrl
140+
..id = iframeId
141+
..name = iframeId
142+
..loading = 'lazy'
143+
..allow = 'clipboard-write';
144+
145+
// Give the caller a chance to modify other attributes of the iframe and
146+
// attach it to their desired location in the document.
147+
onElementCreated(iframe);
148+
149+
await _initializedCompleter.future;
150+
}
151+
152+
/// Updates the source code displayed in the embedded DartPad's editor
153+
/// with the specified Dart [code].
154+
///
155+
/// The [code] should generally be valid Dart code for
156+
/// the latest stable versions of Dart and Flutter.
157+
///
158+
/// Should only be called after [initialize] has completed,
159+
/// otherwise throws.
160+
void updateCode(String code) {
161+
if (!_initialized) {
162+
throw StateError(
163+
'EmbeddedDartPad.initialize must be called and awaited '
164+
'before updating the embedded source code.',
165+
);
166+
}
167+
168+
_underlyingIframe.contentWindowCrossOrigin?.postMessage(
169+
_MessageToDartPad.updateSource(code),
170+
_anyTargetOrigin,
171+
);
172+
}
173+
174+
/// Whether the DartPad instance has been successfully initialized.
175+
///
176+
/// Returns `true` if [initialize] has been called and awaited,
177+
/// and the embedded DartPad has signaled that it's ready to receive messages.
178+
bool get _initialized => _initializedCompleter.isCompleted;
179+
180+
/// Retrieves the iframe element from the current page by
181+
/// searching with its ID of [iframeId].
182+
///
183+
/// If the iframe can't be found, the method throws.
184+
/// The often means it wasn't added to the DOM or was removed.
185+
web.HTMLIFrameElement get _underlyingIframe {
186+
final frame =
187+
web.document.getElementById(iframeId) as web.HTMLIFrameElement?;
188+
if (frame == null) {
189+
throw StateError(
190+
'Failed to find iframe with an '
191+
'id of $iframeId in the document. '
192+
'Have you added the iframe to the document?',
193+
);
194+
}
195+
return frame;
196+
}
197+
}
198+
199+
/// The themes available for an embedded DartPad instance.
200+
enum DartPadTheme {
201+
/// Light theme with a bright background.
202+
light,
203+
204+
/// Dark theme with a dark background.
205+
dark,
206+
207+
/// Theme that relies on DartPad's built-in theme handling.
208+
auto,
209+
}
210+
211+
/// The target origin to be used for cross-frame messages sent to
212+
/// the DartPad iframe's content window.
213+
///
214+
/// Uses '*' to enable communication with DartPad instances
215+
/// regardless of their actual origin.
216+
final JSString _anyTargetOrigin = '*'.toJS;
217+
218+
/// Represents a ready message received from the DartPad iframe.
219+
///
220+
/// Sent by DartPad when it has finished loading and is ready to
221+
/// receive code updates by sending it a cross-frame message.
222+
extension type _EmbedReadyMessage._(JSObject _) {
223+
/// The message type, which should be 'ready' for initialization messages.
224+
external String? get type;
225+
226+
/// The sender ID to identify which DartPad instance sent the message.
227+
external String? get sender;
228+
}
229+
230+
/// Represents DartPad's expected format for receiving cross-frame messages
231+
/// from its parent window, usually the [EmbeddedDartPad] host.
232+
@anonymous
233+
extension type _MessageToDartPad._(JSObject _) implements JSObject {
234+
/// Creates a JavaScript object with the expected structure for
235+
/// updating the source code in an embedded DartPad's editor.
236+
external factory _MessageToDartPad._updateSource({
237+
required String sourceCode,
238+
String type,
239+
});
240+
241+
/// Creates a message to update that can be sent to
242+
/// update the source code in an embedded DartPad instance.
243+
///
244+
/// The [sourceCode] should generally be valid Dart code for
245+
/// the latest stable versions of Dart and Flutter.
246+
factory _MessageToDartPad.updateSource(String sourceCode) =>
247+
_MessageToDartPad._updateSource(
248+
sourceCode: sourceCode,
249+
type: 'sourceCode',
250+
);
251+
}

0 commit comments

Comments
 (0)