Skip to content

Commit de54b5a

Browse files
committed
Enormous rework of (mostly frontend) code.
1 parent 1bd23a9 commit de54b5a

File tree

22 files changed

+21164
-48
lines changed

22 files changed

+21164
-48
lines changed

packages/webio/dist/mux.bundle.js

Lines changed: 11341 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webio/dist/mux.bundle.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webio/package-lock.json

Lines changed: 8577 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webio/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "julia-webio",
3+
"version": "0.1.0",
4+
"description": "Supporting Javascript files for WebIO.jl Julia package",
5+
"repository": "https://github.com/shashi/WebIO.jl",
6+
"author": "Shashi Gowda",
7+
"license": "MIT",
8+
"jupyterlab": {
9+
"extension": "jupyterlab_entry.js"
10+
},
11+
"dependencies": {
12+
"@babel/core": "^7.1.2",
13+
"@babel/polyfill": "^7.0.0",
14+
"@types/debug": "0.0.30",
15+
"debug": "^4.0.1"
16+
},
17+
"devDependencies": {
18+
"babel-loader": "^8.0.4",
19+
"@babel/preset-env": "^7.1.0",
20+
"ts-loader": "^5.2.1",
21+
"tsc": "^1.20150623.0",
22+
"typescript": "^3.1.1",
23+
"webpack": "^4.6",
24+
"webpack-cli": "^2.1"
25+
},
26+
"scripts": {
27+
"build": "webpack"
28+
}
29+
}

packages/webio/src/DomNode.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import debug from "debug";
2+
const log = debug("WebIO:DomNode")
3+
4+
import WebIONode, {WebIODomElement, WebIONodeDataBase, WebIONodeParams, WebIONodeType} from "./Node";
5+
import WebIOScope from "./Scope";
6+
import {createWebIOEventListener} from "./events";
7+
import createNode from "./createNode";
8+
9+
const enum DomNamespace {
10+
// "html" should actually be "http://www.w3.org/1999/xhtml" but it's okay
11+
HTML = "html",
12+
SVG = "http://www.w3.org/2000/svg",
13+
}
14+
15+
/**
16+
* A map of style (CSS) attributes to the associated value.
17+
*/
18+
interface StylesMap {
19+
[attributeName: string]: string;
20+
}
21+
22+
/**
23+
* A map of event names to listeners (or function definitions of listeners).
24+
*/
25+
interface EventsMap {
26+
[eventName: string]: string | EventListener;
27+
}
28+
29+
/**
30+
* A map of (DOM?) attributes to their values (or null if they should be unset).
31+
*/
32+
interface AttributesMap {
33+
[attributeName: string]: string | null;
34+
}
35+
36+
/**
37+
* A map of namespaced (DOM) attributes to their values (or null if they should
38+
* be unset).
39+
*
40+
* This doesn't seem to be implemented on the Julia side of things.
41+
*/
42+
interface AttributesNSMap {
43+
[attributeName: string]: {
44+
namespace: DomNamespace;
45+
value: string | null;
46+
}
47+
}
48+
49+
/**
50+
* Props associated with WebIO DOM nodes.
51+
*/
52+
interface DomNodeProps {
53+
style?: StylesMap;
54+
events?: EventsMap;
55+
attributes?: AttributesMap;
56+
attributesNS?: AttributesNSMap;
57+
setInnerHtml?: string;
58+
59+
/**
60+
* Miscellaneous attributes that will be set on the DOM element.
61+
*/
62+
[otherProp: string]: any;
63+
}
64+
65+
/**
66+
* Data associated with a DOM node.
67+
*/
68+
export interface DomNodeData extends WebIONodeDataBase {
69+
nodeType: WebIONodeType.DOM;
70+
71+
/**
72+
* Information about the type of DOM node (e.g. a <div /> or SVG document).
73+
*/
74+
instanceArgs: {
75+
namespace: DomNamespace;
76+
tag: string;
77+
}
78+
props: DomNodeProps;
79+
}
80+
81+
class WebIODomNode extends WebIONode {
82+
readonly element: WebIODomElement;
83+
children: Array<WebIOScope | WebIODomNode>;
84+
private eventListeners: {[eventType: string]: EventListenerOrEventListenerObject | undefined} = {};
85+
86+
private static createElement(data: DomNodeData) {
87+
const {namespace, tag} = data.instanceArgs;
88+
switch (namespace) {
89+
case DomNamespace.HTML:
90+
return document.createElement(tag);
91+
case DomNamespace.SVG:
92+
return document.createElementNS(DomNamespace.SVG, tag);
93+
default:
94+
throw new Error(`Unknown DOM namespace: ${namespace}.`);
95+
}
96+
}
97+
98+
constructor(nodeData: DomNodeData, options: WebIONodeParams) {
99+
super(nodeData, options);
100+
log("Creating WebIODomNode", {nodeData, options});
101+
this.element = WebIODomNode.createElement(nodeData);
102+
this.applyProps(nodeData.props);
103+
104+
// Create children and append to this node's element.
105+
this.children = nodeData.children.map((nodeData) => (
106+
createNode(nodeData, {webIO: this.webIO, scope: this.scope})
107+
));
108+
for (const child of this.children) {
109+
this.element.appendChild(child.element);
110+
}
111+
}
112+
113+
/**
114+
* Apply "props" to the underlying DOM element.
115+
*
116+
* @param props - The props to apply.
117+
*/
118+
applyProps(props: DomNodeProps) {
119+
log("applyProps", props);
120+
const {style, events, attributes, attributesNS, setInnerHtml, ...rest} = props;
121+
style && this.applyStyles(style);
122+
events && this.applyEvents(events);
123+
attributes && this.applyAttributes(attributes);
124+
attributesNS && this.applyAttributesNS(attributesNS);
125+
setInnerHtml && this.setInnerHTML(setInnerHtml);
126+
this.applyMiscellaneousProps(rest);
127+
}
128+
129+
/**
130+
* Apply all props that don't have special meaning.
131+
*
132+
* This should really be refactored so that all these "miscellaneous" props
133+
* are delivered in a separate object (e.g. have props.miscProps on the same
134+
* level as props.style and props.events et al.).
135+
* @param props - The object of miscellaneous props and their values.
136+
*/
137+
applyMiscellaneousProps(props: {[propName: string]: any}) {
138+
log("applyMiscellaneousProps", props);
139+
for (const propName of Object.keys(props)) {
140+
(this.element as any)[propName] = props[propName];
141+
}
142+
}
143+
144+
applyStyles(styles: StylesMap) {
145+
for (const attributeName of Object.keys(styles)) {
146+
this.element.style[attributeName as any] = styles[attributeName];
147+
}
148+
}
149+
150+
/**
151+
* Apply (add/remove) event listeners to the underlying DOM element.
152+
*
153+
* @param events - A map object from event names to event listeners. If an
154+
* event name is specified (e.g. `click`) that didn't exist before, the
155+
* associated handler (e.g. `events["click"]`) is added as a listener; if
156+
* the event name has already been specified (even if the listener function
157+
* changed!), then nothing happens; if the event name is absent (or null) in
158+
* the map, then any previously setup listeners (if any) are removed.
159+
*/
160+
applyEvents(events: EventsMap) {
161+
for (const eventName of Object.keys(events)) {
162+
const oldListener = this.eventListeners[eventName];
163+
const newListenerSource = events[eventName];
164+
const newListener = newListenerSource && createWebIOEventListener(
165+
this.element,
166+
newListenerSource,
167+
this.scope,
168+
);
169+
170+
if (oldListener && !newListener) {
171+
// We want to just remove the old listener.
172+
this.element.removeEventListener(eventName, oldListener);
173+
delete this.eventListeners[eventName];
174+
} else if (!oldListener && newListener) {
175+
this.element.addEventListener(eventName, newListener);
176+
this.eventListeners[eventName] = newListener;
177+
}
178+
179+
// If the listener is just changed, we don't really handle that.
180+
}
181+
}
182+
183+
/**
184+
* Apply DOM attributes to the underlying DOM element.
185+
*
186+
* @param attributes - The map of attributes to apply.
187+
*/
188+
applyAttributes(attributes: AttributesMap) {
189+
for (const key of Object.keys(attributes)) {
190+
const value = attributes[key];
191+
if (value === null) {
192+
this.element.removeAttribute(key);
193+
} else {
194+
this.element.setAttribute(key, value);
195+
}
196+
}
197+
}
198+
199+
/**
200+
* Apply namespaced DOM attributes to the underlying DOM element.
201+
*
202+
* @param attributes - The `{attributeName: {namespace, value}}` map to apply.
203+
*/
204+
applyAttributesNS(attributes: AttributesNSMap) {
205+
for (const key of Object.keys(attributes)) {
206+
const {namespace, value} = attributes[key];
207+
if (value === null) {
208+
this.element.removeAttributeNS(namespace, key);
209+
} else {
210+
this.element.setAttributeNS(namespace, key, value);
211+
}
212+
}
213+
}
214+
215+
/**
216+
* Set the value associated with the node's element.
217+
*
218+
* This generally only works with `<input />` elements.
219+
*
220+
* @param value
221+
* @throws Will throw an error if the element doesn't have a `value` attribute.
222+
*/
223+
setValue(value: any) {
224+
if ("value" in this.element) {
225+
// If the value hasn't changed, don't re-set it.
226+
if (this.element.value !== value) {
227+
this.element.value = value;
228+
}
229+
} else {
230+
throw new Error("Cannot set value on an HTMLElement that doesn't support it.");
231+
}
232+
}
233+
}
234+
235+
export default WebIODomNode;

packages/webio/src/Node.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import debug from "debug";
2+
const log = debug("WebIO:Node");
3+
4+
import WebIO from "./WebIO";
5+
import WebIOScope, {ScopeNodeData} from "./Scope";
6+
import WebIODomNode, {DomNodeData} from "./DomNode";
7+
8+
/**
9+
* Union of all WebIO supported DOM elements.
10+
*
11+
* Note: we can't use the base Element type for typing purposes because Element
12+
* includes more abstract things than HTML/SVG Elements (that does always have
13+
* the style property, for example).
14+
* We also need HTMLInputElement to stop TypeScript from complaining that
15+
* `value` doesn't exist on an input element, even though it's technically a
16+
* sub-type of HTMLElement; TypeScript will still force us to make sure that the
17+
* type of the element is compatible before accessing the value attribute though.
18+
*/
19+
export type WebIODomElement = HTMLElement | SVGElement | HTMLInputElement;
20+
21+
// This was originally designed thinking that a Scope was a type of node,
22+
// but the thinking on that has changed.
23+
// And now, I'm thinking it has changed back.
24+
export type WebIONodeData = DomNodeData | ScopeNodeData;
25+
export const enum WebIONodeType {
26+
DOM = "DOM",
27+
SCOPE = "Scope", // this one is capitalized for whatever reason
28+
}
29+
30+
/**
31+
* Abstract base interface for "node data."
32+
*
33+
* "Node data" is the data used to construct `WebIONode` instances and is the
34+
* data that is passed over the comm from Julia to the browser.
35+
*/
36+
export interface WebIONodeDataBase {
37+
type: "node";
38+
nodeType: WebIONodeType;
39+
children: WebIONodeData[];
40+
}
41+
42+
export interface WebIONodeParams {
43+
scope?: WebIOScope;
44+
webIO: WebIO;
45+
}
46+
47+
/**
48+
* A high-level "point-of-entry" under which WebIO "things" are rendered.
49+
*
50+
* A `WebIONode` has a root DOM element and some functionality for managing the
51+
* attributes (DOM attributes, CSS styles, event listeners, etc.) that are
52+
* applied to it.
53+
*/
54+
abstract class WebIONode {
55+
abstract readonly element: WebIODomElement;
56+
abstract children: Array<WebIOScope | WebIODomNode>;
57+
readonly scope?: WebIOScope;
58+
readonly webIO: WebIO;
59+
60+
protected constructor(
61+
private readonly nodeData: WebIONodeData,
62+
options: WebIONodeParams,
63+
) {
64+
const {scope, webIO} = options;
65+
this.scope = scope;
66+
this.webIO = webIO;
67+
}
68+
69+
/**
70+
* Set the `innerHTML` attribute of the node's element.
71+
*
72+
* This method will guarantee the execution of `<script />`s which is not done
73+
* by simply setting `element.innerHTML = ...`.
74+
*
75+
* @param html - The HTML string to use; any special HTML characters (`<`, `>`, `&`, etc.)
76+
* should be &-escaped as appropriate (e.g. to set the displayed text to "foo&bar",
77+
* `html` should be `foo&amp;bar`).
78+
*/
79+
setInnerHTML(html: string) {
80+
// In the original WebIO, we like to replace </script> with </_script> because the whole shebang
81+
// is executed inside a <script></script> block (and we don't want to close it too early).
82+
html = html.replace(/<\/_script>/g, "</script>");
83+
84+
log("setInnerHTML", html);
85+
this.element.innerHTML = html;
86+
87+
// If the HTML contained any <script> tags, these are NOT executed when we assign the DOM
88+
// innerHTML attribute, so we have to find-and-replace them to force them to execute.
89+
// We do this weird array coercion because getElementsByTagName returns a
90+
// HTMLCollection object, which updates as the contents of element update
91+
// (creating an infinite loop).
92+
const scripts = [...(this.element.getElementsByTagName("script") as any as HTMLScriptElement[])];
93+
scripts.forEach((oldScript) => {
94+
const newScript = document.createElement("script");
95+
96+
// Copy all attributes.
97+
// Unfortunately, attributes is a NamedNodeMap which doesn't have very
98+
// ES6-like methods of manipulation
99+
for (let i = 0; i < oldScript.attributes.length; ++i) {
100+
const {name, value} = oldScript.attributes[i];
101+
newScript.setAttribute(name, value)
102+
}
103+
104+
// Copy script content
105+
newScript.appendChild(document.createTextNode(oldScript.innerHTML));
106+
107+
// Replace inside DOM
108+
oldScript.parentNode!.replaceChild(oldScript, newScript);
109+
});
110+
}
111+
}
112+
113+
export default WebIONode;

0 commit comments

Comments
 (0)