metaframe-widget 0.3.6__tar.gz → 0.3.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/PKG-INFO +4 -4
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/README.md +3 -3
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/pyproject.toml +1 -1
- metaframe_widget-0.3.7/src/metaframe_widget/_esm.py +177 -0
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/src/metaframe_widget/_widget.py +18 -2
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/tests/test_widget.py +4 -4
- metaframe_widget-0.3.6/src/metaframe_widget/_esm.py +0 -111
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/.gitignore +0 -0
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/LICENSE +0 -0
- {metaframe_widget-0.3.6 → metaframe_widget-0.3.7}/src/metaframe_widget/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metaframe-widget
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: anywidget for embedding metaframe URLs in Jupyter and marimo notebooks
|
|
5
5
|
Project-URL: Homepage, https://github.com/metapages/metaframe-js
|
|
6
6
|
Project-URL: Repository, https://github.com/metapages/metaframe-js
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
# metaframe-widget
|
|
31
31
|
|
|
32
|
-
An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://
|
|
32
|
+
An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://framejs.io/docs) and [metapage](https://docs.metapage.io/) URLs in Jupyter and marimo notebooks.
|
|
33
33
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
@@ -51,7 +51,7 @@ pip install "metaframe-widget[marimo]" # includes marimo
|
|
|
51
51
|
```python
|
|
52
52
|
from metaframe_widget import MetaframeWidget
|
|
53
53
|
|
|
54
|
-
w = MetaframeWidget(url="https://
|
|
54
|
+
w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
|
|
55
55
|
w
|
|
56
56
|
```
|
|
57
57
|
|
|
@@ -61,7 +61,7 @@ w
|
|
|
61
61
|
import marimo as mo
|
|
62
62
|
from metaframe_widget import MetaframeWidget
|
|
63
63
|
|
|
64
|
-
w = MetaframeWidget(url="https://
|
|
64
|
+
w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
|
|
65
65
|
mo.ui.anywidget(w)
|
|
66
66
|
```
|
|
67
67
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# metaframe-widget
|
|
2
2
|
|
|
3
|
-
An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://
|
|
3
|
+
An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://framejs.io/docs) and [metapage](https://docs.metapage.io/) URLs in Jupyter and marimo notebooks.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ pip install "metaframe-widget[marimo]" # includes marimo
|
|
|
22
22
|
```python
|
|
23
23
|
from metaframe_widget import MetaframeWidget
|
|
24
24
|
|
|
25
|
-
w = MetaframeWidget(url="https://
|
|
25
|
+
w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
|
|
26
26
|
w
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ w
|
|
|
32
32
|
import marimo as mo
|
|
33
33
|
from metaframe_widget import MetaframeWidget
|
|
34
34
|
|
|
35
|
-
w = MetaframeWidget(url="https://
|
|
35
|
+
w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
|
|
36
36
|
mo.ui.anywidget(w)
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
ESM = """
|
|
2
|
+
const CDN_URL = "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.8/+esm";
|
|
3
|
+
|
|
4
|
+
let _renderMetapage = null;
|
|
5
|
+
|
|
6
|
+
async function loadRenderMetapage() {
|
|
7
|
+
if (!_renderMetapage) {
|
|
8
|
+
const mod = await import(CDN_URL);
|
|
9
|
+
_renderMetapage = mod.renderMetapage;
|
|
10
|
+
}
|
|
11
|
+
return _renderMetapage;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
async render({ model, el }) {
|
|
16
|
+
// Lay out el as a column: the metapage fills the space, an optional
|
|
17
|
+
// "saved URL" footer sits beneath it (only when there is one to show).
|
|
18
|
+
el.style.display = "flex";
|
|
19
|
+
el.style.flexDirection = "column";
|
|
20
|
+
el.style.boxSizing = "border-box";
|
|
21
|
+
el.style.width = model.get("width") || "100%";
|
|
22
|
+
el.style.height = model.get("height") || "400px";
|
|
23
|
+
|
|
24
|
+
// Inject CSS to remove iframe borders and inline gaps
|
|
25
|
+
const style = document.createElement("style");
|
|
26
|
+
style.textContent = ".metaframe-widget-container { box-sizing: border-box; flex: 1 1 auto; min-height: 0; width: 100%; } .metaframe-widget-container iframe { border: none; display: block; margin: 0; padding: 0; box-sizing: border-box; width: 100%; height: 100%; }";
|
|
27
|
+
el.appendChild(style);
|
|
28
|
+
|
|
29
|
+
const container = document.createElement("div");
|
|
30
|
+
container.className = "metaframe-widget-container";
|
|
31
|
+
el.appendChild(container);
|
|
32
|
+
|
|
33
|
+
// Footer showing the latest short URL produced by editing + saving.
|
|
34
|
+
const footer = document.createElement("div");
|
|
35
|
+
footer.style.cssText = "flex: 0 0 auto; display: none; align-items: center; gap: 6px; padding: 4px 6px; font-family: sans-serif; font-size: 12px; color: #555; background: #f7f7f7; border-top: 1px solid #e0e0e0; overflow: hidden;";
|
|
36
|
+
const footerLabel = document.createElement("span");
|
|
37
|
+
footerLabel.textContent = "Saved:";
|
|
38
|
+
footerLabel.style.flex = "0 0 auto";
|
|
39
|
+
const footerLink = document.createElement("a");
|
|
40
|
+
footerLink.target = "_blank";
|
|
41
|
+
footerLink.rel = "noopener noreferrer";
|
|
42
|
+
footerLink.style.cssText = "flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
|
|
43
|
+
const footerCopy = document.createElement("button");
|
|
44
|
+
footerCopy.textContent = "copy";
|
|
45
|
+
footerCopy.style.cssText = "flex: 0 0 auto; cursor: pointer; font-size: 11px; padding: 1px 6px;";
|
|
46
|
+
footer.appendChild(footerLabel);
|
|
47
|
+
footer.appendChild(footerLink);
|
|
48
|
+
footer.appendChild(footerCopy);
|
|
49
|
+
el.appendChild(footer);
|
|
50
|
+
|
|
51
|
+
function renderSavedUrl() {
|
|
52
|
+
const savedUrl = model.get("saved_url") || "";
|
|
53
|
+
if (savedUrl) {
|
|
54
|
+
footerLink.textContent = savedUrl;
|
|
55
|
+
footerLink.href = savedUrl;
|
|
56
|
+
footer.style.display = "flex";
|
|
57
|
+
} else {
|
|
58
|
+
footer.style.display = "none";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
footerCopy.addEventListener("click", () => {
|
|
62
|
+
const savedUrl = model.get("saved_url") || "";
|
|
63
|
+
if (savedUrl && navigator.clipboard) {
|
|
64
|
+
navigator.clipboard.writeText(savedUrl);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
renderSavedUrl();
|
|
68
|
+
|
|
69
|
+
const renderMetapage = await loadRenderMetapage();
|
|
70
|
+
|
|
71
|
+
let currentResult = null;
|
|
72
|
+
|
|
73
|
+
function applyAllowToIframes() {
|
|
74
|
+
const allow = model.get("allow") || "";
|
|
75
|
+
if (allow) {
|
|
76
|
+
container.querySelectorAll("iframe").forEach(iframe => {
|
|
77
|
+
iframe.allow = allow;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function createMetapage() {
|
|
83
|
+
if (currentResult) {
|
|
84
|
+
currentResult.dispose();
|
|
85
|
+
currentResult = null;
|
|
86
|
+
}
|
|
87
|
+
container.innerHTML = "";
|
|
88
|
+
|
|
89
|
+
const url = model.get("url");
|
|
90
|
+
if (!url) return;
|
|
91
|
+
|
|
92
|
+
const definition = {
|
|
93
|
+
version: "0.3",
|
|
94
|
+
metaframes: {
|
|
95
|
+
mf: { url },
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = await renderMetapage({
|
|
100
|
+
definition,
|
|
101
|
+
rootDiv: container,
|
|
102
|
+
onOutputs: (outputs) => {
|
|
103
|
+
const mfOutputs = outputs.mf || {};
|
|
104
|
+
model.set("outputs", JSON.parse(JSON.stringify(mfOutputs)));
|
|
105
|
+
model.save_changes();
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
currentResult = result;
|
|
109
|
+
|
|
110
|
+
applyAllowToIframes();
|
|
111
|
+
|
|
112
|
+
// Push current inputs if any
|
|
113
|
+
const inputs = model.get("inputs");
|
|
114
|
+
if (inputs && Object.keys(inputs).length > 0) {
|
|
115
|
+
result.setInputs({ mf: inputs });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The embedded editor (same-origin to the URL-shortening worker) mints a
|
|
120
|
+
// short URL when the user clicks "Save and Shorten URL", then postMessages
|
|
121
|
+
// it here. We record it in `saved_url` WITHOUT touching `url`, so the live
|
|
122
|
+
// iframe the user is editing is never torn down and reloaded.
|
|
123
|
+
function expectedOrigin() {
|
|
124
|
+
try {
|
|
125
|
+
return new URL(model.get("url") || "").origin;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const onMessage = (event) => {
|
|
131
|
+
const data = event.data;
|
|
132
|
+
if (!data || data.type !== "metaframe-widget:shorturl" || !data.url) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Route to the correct widget (one notebook may hold several) and
|
|
136
|
+
// verify the message came from our metaframe's origin.
|
|
137
|
+
const iframe = container.querySelector("iframe");
|
|
138
|
+
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
139
|
+
const origin = expectedOrigin();
|
|
140
|
+
if (origin && event.origin !== origin) return;
|
|
141
|
+
|
|
142
|
+
model.set("saved_url", data.url);
|
|
143
|
+
model.save_changes();
|
|
144
|
+
};
|
|
145
|
+
window.addEventListener("message", onMessage);
|
|
146
|
+
|
|
147
|
+
model.on("change:url", createMetapage);
|
|
148
|
+
model.on("change:saved_url", renderSavedUrl);
|
|
149
|
+
|
|
150
|
+
model.on("change:inputs", () => {
|
|
151
|
+
if (currentResult) {
|
|
152
|
+
const inputs = model.get("inputs");
|
|
153
|
+
currentResult.setInputs({ mf: inputs });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
model.on("change:width", () => {
|
|
158
|
+
el.style.width = model.get("width") || "100%";
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
model.on("change:height", () => {
|
|
162
|
+
el.style.height = model.get("height") || "400px";
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
model.on("change:allow", applyAllowToIframes);
|
|
166
|
+
|
|
167
|
+
await createMetapage();
|
|
168
|
+
|
|
169
|
+
return () => {
|
|
170
|
+
window.removeEventListener("message", onMessage);
|
|
171
|
+
if (currentResult) {
|
|
172
|
+
currentResult.dispose();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
"""
|
|
@@ -26,6 +26,9 @@ class MetaframeWidget(anywidget.AnyWidget):
|
|
|
26
26
|
width: CSS width for the widget container (default "100%").
|
|
27
27
|
height: CSS height for the widget container (default "400px").
|
|
28
28
|
allow: iframe allow attribute string (e.g. "camera; microphone").
|
|
29
|
+
saved_url: Latest short URL minted by editing+saving inside the widget
|
|
30
|
+
(read-only). Copy this into your cell to persist edits across
|
|
31
|
+
notebook re-runs.
|
|
29
32
|
"""
|
|
30
33
|
|
|
31
34
|
_esm = ESM
|
|
@@ -36,6 +39,10 @@ class MetaframeWidget(anywidget.AnyWidget):
|
|
|
36
39
|
width = traitlets.Unicode("100%").tag(sync=True)
|
|
37
40
|
height = traitlets.Unicode("400px").tag(sync=True)
|
|
38
41
|
allow = traitlets.Unicode("").tag(sync=True)
|
|
42
|
+
# Set by the ESM when the user clicks "Save and Shorten URL" inside the
|
|
43
|
+
# embedded editor. Read-only from Python's perspective; updating it does not
|
|
44
|
+
# reload the iframe (so editing is not interrupted).
|
|
45
|
+
saved_url = traitlets.Unicode("").tag(sync=True)
|
|
39
46
|
|
|
40
47
|
def __init__(self, *args, **kwargs):
|
|
41
48
|
super().__init__(*args, **kwargs)
|
|
@@ -67,6 +74,15 @@ class MetaframeWidget(anywidget.AnyWidget):
|
|
|
67
74
|
"""
|
|
68
75
|
self.observe(callback, names=["outputs"])
|
|
69
76
|
|
|
77
|
+
def on_saved_url_change(self, callback):
|
|
78
|
+
"""Register a callback for when the user saves edits to a new short URL.
|
|
79
|
+
|
|
80
|
+
Fires after the user clicks "Save and Shorten URL" inside the embedded
|
|
81
|
+
editor. The callback receives a change dict whose 'new' key holds the
|
|
82
|
+
freshly-minted short URL.
|
|
83
|
+
"""
|
|
84
|
+
self.observe(callback, names=["saved_url"])
|
|
85
|
+
|
|
70
86
|
@classmethod
|
|
71
87
|
def from_code(
|
|
72
88
|
cls,
|
|
@@ -78,7 +94,7 @@ class MetaframeWidget(anywidget.AnyWidget):
|
|
|
78
94
|
) -> "MetaframeWidget":
|
|
79
95
|
"""Create a MetaframeWidget from raw JavaScript code.
|
|
80
96
|
|
|
81
|
-
Encodes the code into a
|
|
97
|
+
Encodes the code into a framejs.io URL with the code in the hash.
|
|
82
98
|
|
|
83
99
|
Args:
|
|
84
100
|
js_code: JavaScript source for the metaframe.
|
|
@@ -87,7 +103,7 @@ class MetaframeWidget(anywidget.AnyWidget):
|
|
|
87
103
|
allow: iframe allow attribute string (e.g. "camera; microphone").
|
|
88
104
|
"""
|
|
89
105
|
encoded = string_to_base64_string(js_code)
|
|
90
|
-
url = f"https://
|
|
106
|
+
url = f"https://framejs.io/#?js={encoded}"
|
|
91
107
|
return cls(url=url, width=width, height=height, allow=allow, **kwargs)
|
|
92
108
|
|
|
93
109
|
def pipe_to(self, target: "MetaframeWidget", output_key: str, input_key: str = None):
|
|
@@ -5,8 +5,8 @@ from metaframe_widget import MetaframeWidget
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def test_create_widget():
|
|
8
|
-
w = MetaframeWidget(url="https://
|
|
9
|
-
assert w.url == "https://
|
|
8
|
+
w = MetaframeWidget(url="https://framejs.io/#?js=abc")
|
|
9
|
+
assert w.url == "https://framejs.io/#?js=abc"
|
|
10
10
|
assert w.inputs == {}
|
|
11
11
|
assert w.outputs == {}
|
|
12
12
|
assert w.height == "400px"
|
|
@@ -34,14 +34,14 @@ def test_from_code():
|
|
|
34
34
|
# from_code does: encodeURIComponent(code) then base64
|
|
35
35
|
encoded_uri = urllib.parse.quote(code, safe="-_.!~*'()")
|
|
36
36
|
encoded = base64.b64encode(encoded_uri.encode("ascii")).decode("ascii")
|
|
37
|
-
expected_url = f"https://
|
|
37
|
+
expected_url = f"https://framejs.io/#?js={encoded}"
|
|
38
38
|
assert w.url == expected_url
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def test_from_code_with_kwargs():
|
|
42
42
|
w = MetaframeWidget.from_code("code", height="600px")
|
|
43
43
|
assert w.height == "600px"
|
|
44
|
-
assert "
|
|
44
|
+
assert "framejs.io" in w.url
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def test_on_outputs_change():
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
ESM = """
|
|
2
|
-
const CDN_URL = "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.8/+esm";
|
|
3
|
-
|
|
4
|
-
let _renderMetapage = null;
|
|
5
|
-
|
|
6
|
-
async function loadRenderMetapage() {
|
|
7
|
-
if (!_renderMetapage) {
|
|
8
|
-
const mod = await import(CDN_URL);
|
|
9
|
-
_renderMetapage = mod.renderMetapage;
|
|
10
|
-
}
|
|
11
|
-
return _renderMetapage;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export default {
|
|
15
|
-
async render({ model, el }) {
|
|
16
|
-
// Size el explicitly so marimo and Jupyter honour width/height
|
|
17
|
-
el.style.display = "block";
|
|
18
|
-
el.style.boxSizing = "border-box";
|
|
19
|
-
el.style.width = model.get("width") || "100%";
|
|
20
|
-
el.style.height = model.get("height") || "400px";
|
|
21
|
-
|
|
22
|
-
// Inject CSS to remove iframe borders and inline gaps
|
|
23
|
-
const style = document.createElement("style");
|
|
24
|
-
style.textContent = ".metaframe-widget-container { box-sizing: border-box; width: 100%; height: 100%; } .metaframe-widget-container iframe { border: none; display: block; margin: 0; padding: 0; box-sizing: border-box; width: 100%; height: 100%; }";
|
|
25
|
-
el.appendChild(style);
|
|
26
|
-
|
|
27
|
-
const container = document.createElement("div");
|
|
28
|
-
container.className = "metaframe-widget-container";
|
|
29
|
-
container.style.width = "100%";
|
|
30
|
-
container.style.height = "100%";
|
|
31
|
-
el.appendChild(container);
|
|
32
|
-
|
|
33
|
-
const renderMetapage = await loadRenderMetapage();
|
|
34
|
-
|
|
35
|
-
let currentResult = null;
|
|
36
|
-
|
|
37
|
-
function applyAllowToIframes() {
|
|
38
|
-
const allow = model.get("allow") || "";
|
|
39
|
-
if (allow) {
|
|
40
|
-
container.querySelectorAll("iframe").forEach(iframe => {
|
|
41
|
-
iframe.allow = allow;
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function createMetapage() {
|
|
47
|
-
if (currentResult) {
|
|
48
|
-
currentResult.dispose();
|
|
49
|
-
currentResult = null;
|
|
50
|
-
}
|
|
51
|
-
container.innerHTML = "";
|
|
52
|
-
|
|
53
|
-
const url = model.get("url");
|
|
54
|
-
if (!url) return;
|
|
55
|
-
|
|
56
|
-
const definition = {
|
|
57
|
-
version: "0.3",
|
|
58
|
-
metaframes: {
|
|
59
|
-
mf: { url },
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const result = await renderMetapage({
|
|
64
|
-
definition,
|
|
65
|
-
rootDiv: container,
|
|
66
|
-
onOutputs: (outputs) => {
|
|
67
|
-
const mfOutputs = outputs.mf || {};
|
|
68
|
-
model.set("outputs", JSON.parse(JSON.stringify(mfOutputs)));
|
|
69
|
-
model.save_changes();
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
currentResult = result;
|
|
73
|
-
|
|
74
|
-
applyAllowToIframes();
|
|
75
|
-
|
|
76
|
-
// Push current inputs if any
|
|
77
|
-
const inputs = model.get("inputs");
|
|
78
|
-
if (inputs && Object.keys(inputs).length > 0) {
|
|
79
|
-
result.setInputs({ mf: inputs });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
model.on("change:url", createMetapage);
|
|
84
|
-
|
|
85
|
-
model.on("change:inputs", () => {
|
|
86
|
-
if (currentResult) {
|
|
87
|
-
const inputs = model.get("inputs");
|
|
88
|
-
currentResult.setInputs({ mf: inputs });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
model.on("change:width", () => {
|
|
93
|
-
el.style.width = model.get("width") || "100%";
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
model.on("change:height", () => {
|
|
97
|
-
el.style.height = model.get("height") || "400px";
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
model.on("change:allow", applyAllowToIframes);
|
|
101
|
-
|
|
102
|
-
await createMetapage();
|
|
103
|
-
|
|
104
|
-
return () => {
|
|
105
|
-
if (currentResult) {
|
|
106
|
-
currentResult.dispose();
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|