Skip to content

Commit 6455b04

Browse files
committed
Add a public library to inject_dartpad
1 parent 88aa84d commit 6455b04

File tree

6 files changed

+1602
-962
lines changed

6 files changed

+1602
-962
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:inject_dartpad/inject_dartpad.dart';
2+
import 'package:web/web.dart' as web;
3+
4+
void main() async {
5+
final dartPad = EmbeddedDartPad.create(
6+
iframeId: 'my-dartpad',
7+
theme: DartPadTheme.light,
8+
);
9+
10+
await dartPad.initialize(
11+
addToDocument: (iframe) {
12+
iframe.style.height = '560';
13+
14+
web.document.body!.append(iframe);
15+
},
16+
);
17+
18+
dartPad.updateCode('''
19+
void main() {
20+
print("Hello, I'm Dash!");
21+
}''');
22+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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+
/// addToDocument: (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 [addToDocument] 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+
/// addToDocument: (iframe) {
108+
/// document.getElementById('dartpad-container')!.append(iframe);
109+
/// },
110+
/// );
111+
/// ```
112+
Future<void> initialize({
113+
required void Function(web.HTMLIFrameElement iframe) addToDocument,
114+
}) async {
115+
if (_initialized) return;
116+
117+
late final JSExportedDartFunction readyHandler;
118+
readyHandler = (web.MessageEvent event) {
119+
if (event.data case _EmbedReadyMessage(type: 'ready', :final sender?)) {
120+
if (sender != iframeId) {
121+
return;
122+
}
123+
124+
web.window.removeEventListener('message', readyHandler);
125+
if (!_initialized) {
126+
_initializedCompleter.complete();
127+
}
128+
}
129+
}.toJS;
130+
131+
web.window.addEventListener('message', readyHandler);
132+
133+
final iframe = web.HTMLIFrameElement()
134+
..src = _iframeUrl
135+
..id = iframeId
136+
..name = iframeId
137+
..loading = 'lazy'
138+
..allow = 'clipboard-write';
139+
addToDocument(iframe);
140+
141+
await _initializedCompleter.future;
142+
}
143+
144+
/// Updates the source code displayed in the embedded DartPad's editor
145+
/// with the specified Dart [code].
146+
///
147+
/// The [code] should generally be valid Dart code for
148+
/// the latest stable versions of Dart and Flutter.
149+
///
150+
/// Should only be called after [initialize] has completed,
151+
/// otherwise throws.
152+
void updateCode(String code) {
153+
if (!_initialized) {
154+
throw StateError(
155+
'EmbeddedDartPad.initialize must be called and awaited '
156+
'before updating the embedded source code.',
157+
);
158+
}
159+
160+
_underlyingIframe.contentWindowCrossOrigin?.postMessage(
161+
_MessageToDartPad.updateSource(code),
162+
_anyTargetOrigin,
163+
);
164+
}
165+
166+
/// Whether the DartPad instance has been successfully initialized.
167+
///
168+
/// Returns `true` if [initialize] has been called and awaited,
169+
/// and the embedded DartPad has signaled that it's ready to receive messages.
170+
bool get _initialized => _initializedCompleter.isCompleted;
171+
172+
/// Retrieves the iframe element from the current page by
173+
/// searching with its ID of [iframeId].
174+
///
175+
/// If the iframe can't be found, the method throws.
176+
/// The often means it wasn't added to the DOM or was removed.
177+
web.HTMLIFrameElement get _underlyingIframe {
178+
final frame =
179+
web.document.getElementById(iframeId) as web.HTMLIFrameElement?;
180+
if (frame == null) {
181+
throw StateError(
182+
'Failed to find iframe with an '
183+
'id of $iframeId in the document. '
184+
'Have you added the iframe to the document?',
185+
);
186+
}
187+
return frame;
188+
}
189+
}
190+
191+
/// The themes available for an embedded DartPad instance.
192+
enum DartPadTheme {
193+
/// Light theme with a bright background.
194+
light,
195+
196+
/// Dark theme with a dark background.
197+
dark,
198+
199+
/// Theme that relies on DartPad's built-in theme handling.
200+
auto,
201+
}
202+
203+
/// The target origin to be used for cross-frame messages sent to
204+
/// the DartPad iframe's content window.
205+
///
206+
/// Uses '*' to enable communication with DartPad instances
207+
/// regardless of their actual origin.
208+
final JSString _anyTargetOrigin = '*'.toJS;
209+
210+
/// Represents a ready message received from the DartPad iframe.
211+
///
212+
/// Sent by DartPad when it has finished loading and is ready to
213+
/// receive code updates by sending it a cross-frame message.
214+
extension type _EmbedReadyMessage._(JSObject _) {
215+
/// The message type, which should be 'ready' for initialization messages.
216+
external String? get type;
217+
218+
/// The sender ID to identify which DartPad instance sent the message.
219+
external String? get sender;
220+
}
221+
222+
/// Represents DartPad's expected format for receiving cross-frame messages
223+
/// from its parent window, usually the [EmbeddedDartPad] host.
224+
@anonymous
225+
extension type _MessageToDartPad._(JSObject _) implements JSObject {
226+
/// Creates a JavaScript object with the expected structure for
227+
/// updating the source code in an embedded DartPad's editor.
228+
external factory _MessageToDartPad._updateSource({
229+
required String sourceCode,
230+
String type,
231+
});
232+
233+
/// Creates a message to update that can be sent to
234+
/// update the source code in an embedded DartPad instance.
235+
///
236+
/// The [sourceCode] should generally be valid Dart code for
237+
/// the latest stable versions of Dart and Flutter.
238+
factory _MessageToDartPad.updateSource(String sourceCode) =>
239+
_MessageToDartPad._updateSource(
240+
sourceCode: sourceCode,
241+
type: 'sourceCode',
242+
);
243+
}

0 commit comments

Comments
 (0)