8000 Merge pull request #36 from anaconda/pys-18/add-pylist · Linux4SA/pyscript@d54d268 · GitHub
[go: up one dir, main page]

Skip to content

Commit d54d268

Browse files
authored
Merge pull request pyscript#36 from anaconda/pys-18/add-pylist
[PYS-18] Add pylist
2 parents 518471b + 4065c13 commit d54d268

File tree

13 files changed

+598
-13
lines changed

13 files changed

+598
-13
lines changed

pyscriptjs/examples/pylist.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from datetime import datetime as dt
2+
3+
class PyItem(PyItemTemplate):
4+
def on_click(self, evt=None):
5+
self.data['done'] = not self.data['done']
6+
self.strike(self.data['done'])
7+
8+
self.select('input').element.checked = self.data['done']
9+
10+
class PyList(PyListTemplate):
11+
item_class = PyItem
12+
13+
def add_task(*ags, **kws):
14+
# create a new dictionary representing the new task
15+
task = { "content": new_task_content.value, "done": False, "created_at": dt.now() }
16+
17+
# add a new task to the list and tell it to use the `content` key to show in the UI
18+
# and to use the key `done` to sync the task status with a checkbox element in the UI
19+
myList.add(task, labels=['content'], state_key="done")
20+
21+
# clear the inputbox element used to create the new task
22+
new_task_content.clear()

pyscriptjs/examples/todo-pylist.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
6+
<title>Todo App</title>
7+
8+
<link rel="icon" type="image/png" href="favicon.png" />
9+
<link rel="stylesheet" href="/build/pyscript.css" />
10+
11+
<script defer src="/build/pyscript.js"></script>
12+
<py-env>
13+
- paths:
14+
- /utils.py
15+
</py-env>
16+
<py-register-widget src="/pylist.py" name="py-list" klass="PyLis F438 t"></py-register-widget>
17+
</head>
18+
19+
<body>
20+
<py-title>To Do List</py-title>
21+
<py-box widths="4/5;1/5">
22+
<py-inputbox id="new-task-content">
23+
def on_keypress(e):
24+
if (e.code == "Enter"):
25+
add_task()
26+
</py-inputbox>
27+
<py-button id="new-task-btn" label="Add Task!">
28+
def on_click(evt):
29+
add_task()
30+
</button>
31+
</py-box>
32+
33+
<py-list id="myList"></py-list>
34+
<py-repl id="my-repl" auto-generate="true"> </py-repl>
35+
</body>
36+
</html>

pyscriptjs/src/App.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
for (let initializer of $postInitializers){
5454
initializer();
5555
}
56-
}, 5000);
56+
}, 3000);
5757
5858
}
5959

pyscriptjs/src/components/base.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,174 @@ export class BaseEvalElement extends HTMLElement {
149149
this.errorElement.hidden = false;
150150
this.errorElement.style.display = 'block';
151151
}
152+
} // end evaluate
153+
154+
async eval(source: string): Promise<void> {
155+
let output;
156+
let pyodide = await pyodideReadyPromise;
157+
158+
try{
159+
output = await pyodide.runPythonAsync(source);
160+
if (output !== undefined){ console.log(output); }
161+
} catch (err) {
162+
console.log(err);
163+
}
164+
} // end eval
165+
}
166+
167+
function createWidget(name: string, code: string, klass: string){
168+
169+
class CustomWidget extends HTMLElement{
170+
shadow: ShadowRoot;
171+
wrapper: HTMLElement;
172+
173+
name: string = name;
174+
klass: string = klass;
175+
code: string = code;
176+
proxy: any;
177+
proxyClass: any;
178+
179+
constructor() {
180+
super();
181+
182+
// attach shadow so we can preserve the element original innerHtml content
183+
this.shadow = this.attachShadow({ mode: 'open'});
184+
185+
this.wrapper = document.createElement('slot');
186+
this.shadow.appendChild(this.wrapper);
187+
}
188+
189+
connectedCallback() {
190+
// TODO: we are calling with a 2secs delay to allow pyodide to load
191+
// ideally we can just wait for it to load and then run. To do
192+
// so we need to replace using the promise and actually using
193+
// the interpreter after it loads completely
194+
setTimeout(() => {
195+
this.eval(this.code).then(() => {
196+
this.proxy = this.proxyClass(this);
197+
console.log('proxy', this.proxy);
198+
this.proxy.connect();
199+
this.registerWidget();
200+
});
201+
}, 2000);
202+
}
203+
204+
async registerWidget(){
205+
let pyodide = await pyodideReadyPromise;
206+
console.log('new widget registered:', this.name);
207+
pyodide.globals.set(this.id, this.proxy);
208+
}
209+
210+
async eval(source: string): Promise<void> {
211+
let output;
212+
let pyodide = await pyodideReadyPromise;
213+
try{
214+
output = await pyodide.runPythonAsync(source);
215+
this.proxyClass = pyodide.globals.get(this.klass);
216+
if (output !== undefined){
217+
console.log(output);
218+
}
219+
} catch (err) {
220+
console.log(err);
221+
}
222+
}
223+
}
224+
let xPyWidget = customElements.define(name, CustomWidget);
225+
}
226+
227+
export class PyWidget extends HTMLElement {
228+
shadow: ShadowRoot;
229+
name: string;
230+
klass: string;
231+
outputElement: HTMLElement;
232+
errorElement: HTMLElement;
233+
wrapper: HTMLElement;
234+
theme: string;
235+
source: string;
236+
code: string;
237+
238+
constructor() {
239+
super();
240+
241+
// attach shadow so we can preserve the element original innerHtml content
242+
this.shadow = this.attachShadow({ mode: 'open'});
243+
244+
this.wrapper = document.createElement('slot');
245+
this.shadow.appendChild(this.wrapper);
246+
247+
if (this.hasAttribute('src')) {
248+
this.source = this.getAttribute('src');
249+
}
250+
251+
if (this.hasAttribute('name')) {
252+
this.name = this.getAttribute('name');
253+
}
254+
255+
if (this.hasAttribute('klass')) {
256+
this.klass = this.getAttribute('klass');
257+
}
152258
}
259+
260+
connectedCallback() {
261+
if (this.id === undefined){
262+
throw new ReferenceError(`No id specified for component. Components must have an explicit id. Please use id="" to specify your component id.`)
263+
return;
264+
}
265+
266+
let mainDiv = document.createElement('div');
267+
mainDiv.id = this.id + '-main';
268+
this.appendChild(mainDiv);
269+
console.log('reading source')
270+
this.getSourceFromFile(this.source).then((code:string) => {
271+
this.code = code;
272+
createWidget(this.name, code, this.klass);
273+
});
274+
}
275+
276+
initOutErr(): void {
277+
if (this.hasAttribute('output')) {
278+
this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output'));
279+
280+
// in this case, the default output-mode is append, if hasn't been specified
281+
if (!this.hasAttribute('output-mode')) {
282+
this.setAttribute('output-mode', 'append');
283+
}
284+
}else{
285+
if (this.hasAttribute('std-out')){
286+
this.outputElement = document.getElementById(this.getAttribute('std-out'));
287+
}else{
288+
// In this case neither output or std-out have been provided so we need
289+
// to create a new output div to output to
290+
this.outputElement = document.createElement('div');
291+
this.outputElement.classList.add("output");
292+
this.outputElement.hidden = true;
293+
this.outputElement.id = this.id + "-" + this.getAttribute("exec-id");
294+
}
295+
296+
if (this.hasAttribute('std-err')){
297+
this.outputElement = document.getElementById(this.getAttribute('std-err'));
298+
}else{
299+
this.errorElement = this.outputElement;
300+
}
301+
}
302+
}
303+
304+
async getSourceFromFile(s: string): Promise<string>{
305+
let pyodide = await pyodideReadyPromise;
306+
let response = await fetch(s);
307+
return await response.text();
308+
}
309+
310+
async eval(source: string): Promise<void> {
311+
let output;
312+
let pyodide = await pyodideReadyPromise;
313+
try{
314+
output = await pyodide.runPythonAsync(source);
315+
if (output !== undefined){
316+
console.log(output);
317+
}
318+
} catch (err) {
319+
console.log(err);
320+
}
321+
}
153322
}

pyscriptjs/src/components/pybox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class PyBox extends HTMLElement {
1919

2020
connectedCallback() {
2121
let mainDiv = document.createElement('div');
22-
addClasses(mainDiv, ["flex"])
22+
addClasses(mainDiv, ["flex", "mx-8"])
2323

2424
// Hack: for some reason when moving children, the editor box duplicates children
2525
// meaning that we end up with 2 editors, if there's a <py-repl> inside the <py-box>

pyscriptjs/src/components/pybutton.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { BaseEvalElement } from './base';
2+
import { addClasses, ltrim, htmlDecode } from '../utils';
3+
4+
export class PyButton extends BaseEvalElement {
5+
shadow: ShadowRoot;
6+
wrapper: HTMLElement;
7+
theme: string;
8+
widths: Array<string>;
9+
label: string;
10+
mount_name: string;
11+
constructor() {
12+
super();
13+
14+
if (this.hasAttribute('label')) {
15+
this.label = this.getAttribute('label');
16+
}
17+
}
18+
19+
connectedCallback() {
20+
this.code = htmlDecode(this.innerHTML);
21+
this.mount_name = this.id.split("-").join("_");
22+
this.innerHTML = '';
23+
24+
let mainDiv = document.createElement('button');
25+
mainDiv.innerHTML = this.label;
26+
addClasses(mainDiv, ["p-2", "text-white", "bg-blue-600", "border", "border-blue-600", "rounded"]);
27+
28+
mainDiv.id = this.id;
29+
this.id = `${this.id}-container`;
30+
31+
this.appendChild(mainDiv);
32+
this.code = this.code.split("self").join(this.mount_name);
33+
let registrationCode = `${this.mount_name} = Element("${ mainDiv.id }")`;
34+
if (this.code.includes("def on_focus")){
35+
this.code = this.code.replace("def on_focus", `def on_focus_${this.mount_name}`);
36+
registrationCode += `\n${this.mount_name}.element.onfocus = on_focus_${this.mount_name}`
37+
}
38+
39+
if (this.code.includes("def on_click")){
40+
this.code = this.code.replace("def on_click", `def on_click_${this.mount_name}`);
41+
registrationCode += `\n${this.mount_name}.element.onclick = on_click_${this.mount_name}`
42+
}
43+
44+
// now that we appended and the element is attached, lets connect with the event handlers
45+
// defined for this widget
46+
setTimeout(() => {
47+
this.eval(this.code).then(() => {
48+
this.eval(registrationCode).then(() => {
49+
console.log('registered handlers');
50+
});
51+
});
52+
}, 4000);
53+
54+
console.log('py-button connected');
55+
}
56+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { BaseEvalElement } from './base';
2+
import { addClasses, ltrim, htmlDecode } from '../utils';
3+
4+
export class PyInputBox extends BaseEvalElement {
5+
shadow: ShadowRoot;
6+
wrapper: HTMLElement;
7+
theme: string;
8+
widths: Array<string>;
9+
label: string;
10+
mount_name: string;
11+
constructor() {
12+
super();
13+
14+
if (this.hasAttribute('label')) {
15+
this.label = this.getAttribute('label');
16+
}
17+
}
18+
19+
connectedCallback() {
20+
this.code = htmlDecode(this.innerHTML);
21+
this.mount_name = this.id.split("-").join("_");
22+
this.innerHTML = '';
23+
24+
let mainDiv = document.createElement('input');
25+
mainDiv.type = "text";
26+
addClasses(mainDiv, ["border", "flex-1", "w-full", "mr-3", "border-gray-300", "p-2", "rounded"]);
27+
28+
mainDiv.id = this.id;
29+
this.id = `${this.id}-container`;
30+
this.appendChild(mainDiv);
31+
32+
// now that we appended and the element is attached, lets connect with the event handlers
33+
// defined for this widget
34+
this.appendChild(mainDiv);
35+
this.code = this.code.split("self").join(this.mount_name);
36+
let registrationCode = `${this.mount_name} = Element("${ mainDiv.id }")`;
37+
if (this.code.includes("def on_keypress")){
38+
this.code = this.code.replace("def on_keypress", `def on_keypress_${this.mount_name}`);
39+
registrationCode += `\n${this.mount_name}.element.onkeypress = on_keypress_${this.mount_name}`
40+
}
41+
42+
// TODO: For now we delay execution to allow pyodide to load but in the future this
43+
// should really wait for it to load..
44+
setTimeout(() => {
45+
this.eval(this.code).then(() => {
46+
this.eval(registrationCode).then(() => {
47+
console.log('registered handlers');
48+
});
49+
});
50+
}, 4000);
51+
}
52+
}
53+
54+

pyscriptjs/src/components/pyrepl.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class PyRepl extends BaseEvalElement {
9898
})
9999

100100
let mainDiv = document.createElement('div');
101-
addClasses(mainDiv, ["parentBox", "group", "flex", "flex-col", "mt-2", "border-2", "border-gray-200", "rounded-lg"])
101+
addClasses(mainDiv, ["parentBox", "group", "flex", "flex-col", "mt-2", "border-2", "border-gray-200", "rounded-lg", "mx-8"])
102102
// add Editor to main PyScript div
103103

104104
// Butons DIV
@@ -199,6 +199,10 @@ export class PyRepl extends BaseEvalElement {
199199
}
200200

201201
postEvaluate(): void {
202+
203+
this.outputElement.hidden = false;
204+
this.outputElement.style.display = 'block';
205+
202206
if (this.hasAttribute('auto-generate')) {
203207
let nextExecId = parseInt(this.getAttribute('exec-id')) + 1;
204208
const newPyRepl = document.createElement("py-repl");

0 commit comments

Comments
 (0)
0