-
Notifications
You must be signed in to change notification settings - Fork 49
/
editable-wat.ts
274 lines (237 loc) · 8.5 KB
/
editable-wat.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import type { EditorView } from "codemirror";
import * as featureDetector from "./editor-libs/feature-detector.js";
import mceConsole from "./editor-libs/console.js";
import * as mceEvents from "./editor-libs/events.js";
import { watify } from "watify";
import "../css/editor-libs/ui-fonts.css";
import "../css/editor-libs/common.css";
import "../css/editable-js-and-wat.css";
import "../css/editor-libs/tabby.css";
import {
getEditorContent,
initCodeEditor,
languageJavaScript,
languageWAST,
} from "./editor-libs/codemirror-editor.js";
(async function () {
const watCodeBlock = document.getElementById("static-wat") as HTMLElement;
const jsCodeBlock = document.getElementById("static-js") as HTMLElement;
const exampleFeature = watCodeBlock.dataset["feature"];
const execute = document.getElementById("execute") as HTMLElement;
const output = document.querySelector("#console code") as HTMLElement;
const reset = document.getElementById("reset") as HTMLElement;
const tabContainer = document.getElementById("tab-container") as HTMLElement;
const tabs =
tabContainer.querySelectorAll<HTMLButtonElement>("button[role='tab']");
const tabList = document.getElementById("tablist") as HTMLElement;
let watCodeMirror: EditorView | null;
let jsCodeMirror: EditorView | null;
let liveContainer;
let staticContainer;
/**
* Hides all tabpanels
*/
function hideTabPanels() {
// get all section with a role of tabpanel
const tabPanels = tabContainer.querySelectorAll("[role='tabpanel']");
// hide all tabpanels
for (const panel of tabPanels) {
panel.classList.add("hidden");
}
}
function registerEventListeners() {
tabList.addEventListener("click", (event) => {
const eventTarget = event.target as typeof tabList;
const role = eventTarget.getAttribute("role");
if (role === "tab") {
const activeTab = tabList.querySelector(
"button[aria-selected='true']",
) as HTMLElement;
const controls = eventTarget.getAttribute("aria-controls") || "";
const selectedPanel = document.getElementById(controls) as HTMLElement;
hideTabPanels();
setActiveTab(eventTarget, activeTab);
// now show the selected tabpanel
selectedPanel.classList.remove("hidden");
selectedPanel.setAttribute("aria-hidden", "false");
}
});
tabList.addEventListener("keyup", (event) => {
event.stopPropagation();
switch (event.key) {
case "ArrowRight":
case "ArrowDown":
setNextActiveTab("forward");
break;
case "ArrowLeft":
case "ArrowUp":
setNextActiveTab("reverse");
break;
case "Home":
setActiveTab(tabs[0]);
break;
case "End":
setActiveTab(tabs[tabs.length - 1]);
break;
case "default":
return;
}
});
}
/**
* Sets the newly activated tab as active, and ensures that
* the previously active tab is unset.
* @param {Object} nextActiveTab - The tab to activate
* @param {Object} [activeTab] - The current active tab
*/
function setActiveTab(nextActiveTab: HTMLElement, activeTab?: HTMLElement) {
if (activeTab) {
// set the currentSelectedTab to false
activeTab.setAttribute("aria-selected", "false");
activeTab.setAttribute("tabindex", "-1");
}
// set the activated tab to selected
nextActiveTab.setAttribute("aria-selected", "true");
nextActiveTab.removeAttribute("tabindex");
nextActiveTab.focus();
}
/**
* Handles moving focus and activating the next tab in either direction,
* based on arrow key events
* @param {String} direction - The direction in which to move tab focus
* Must be either forward, or reverse.
*/
function setNextActiveTab(direction: string) {
const activeTab = tabList.querySelector(
"button[aria-selected='true']",
) as HTMLElement;
// if the direction specified is not valid, simply return
if (direction !== "forward" && direction !== "reverse") {
return;
}
if (direction === "forward") {
if (activeTab.nextElementSibling instanceof HTMLElement) {
setActiveTab(activeTab.nextElementSibling, activeTab);
activeTab.nextElementSibling.click();
} else {
// reached the last tab, loop back to the first tab
setActiveTab(tabs[0]);
tabs[0].click();
}
} else if (direction === "reverse") {
if (activeTab.previousElementSibling instanceof HTMLElement) {
setActiveTab(activeTab.previousElementSibling, activeTab);
activeTab.previousElementSibling.click();
} else {
// reached the first tab, loop around to the last tab
setActiveTab(tabs[tabs.length - 1]);
tabs[tabs.length - 1].click();
}
}
}
/**
* Reads the textContent from the interactiveCodeBlock, sends the
* textContent to executeLiveExample, and logs the output to the
* output container
*/
function applyCode() {
if (!(watCodeMirror && jsCodeMirror)) {
initCodeMirror();
// "as EditorView" on next lines needed to trick TypeScript
}
const wat = getEditorContent(watCodeMirror as EditorView);
const js = getEditorContent(jsCodeMirror as EditorView);
updateOutput(wat, js);
}
/**
* Initialize CodeMirror
*/
function initCodeMirror() {
const watContainer = document.getElementById("wat-editor") as HTMLElement;
watCodeMirror = initCodeEditor(
watContainer,
watCodeBlock.textContent || "",
languageWAST(),
);
const jsContainer = document.getElementById("js-editor") as HTMLElement;
jsCodeMirror = initCodeEditor(
jsContainer,
jsCodeBlock.textContent || "",
languageJavaScript(),
);
}
/**
* Initialize the interactive editor
*/
function initInteractiveEditor() {
/* If the `data-height` attribute is defined on the `codeBlock`, set
the value of this attribute as a class on the editor element. */
if (watCodeBlock.dataset["height"]) {
const watEditor = document.getElementById("wat-panel") as HTMLElement;
watEditor.classList.add(watCodeBlock.dataset["height"]);
const jsEditor = document.getElementById("js-panel") as HTMLElement;
jsEditor.classList.add(watCodeBlock.dataset["height"]);
}
staticContainer = document.getElementById("static") as HTMLElement;
staticContainer.classList.add("hidden");
liveContainer = document.getElementById("live") as HTMLElement;
liveContainer.classList.remove("hidden");
mceConsole();
mceEvents.register();
initCodeMirror();
registerEventListeners();
}
/**
* compiles the wat code to wasm
* @param {string} wat
* @returns {Blob} a blob with the newly created wasm module
*/
async function compileWat(wat: string): Promise<Blob> {
const binary = await watify(wat);
const blob = new Blob([binary], { type: "application/wasm" });
return blob;
}
/**
* Executes the provided code snippet and logs the result
* to the output container.
* @param {String} wat - The wat code to execute
* @param {String} js - The JavaScript code to execute
*/
async function updateOutput(wat: string, js: string) {
output.classList.add("fade-in");
try {
const watBlob = await compileWat(wat);
const watUrl = URL.createObjectURL(watBlob);
const exampleCode = js.replaceAll("{%wasm-url%}", watUrl);
// Create an new async function from the code, and immediately execute it.
// using an async function since WebAssembly.instantiate is async and
// we need to await in order to capture errors
const AsyncFunction = Object.getPrototypeOf(
// eslint-disable-next-line @typescript-eslint/no-empty-function
async function () {},
).constructor;
await new AsyncFunction(exampleCode)();
} catch (error) {
console.error(error);
}
output.addEventListener("animationend", () =>
output.classList.remove("fade-in"),
);
}
/* only execute code in supported browsers */
if ("WebAssembly" in window && featureDetector.isDefined(exampleFeature)) {
document.documentElement.classList.add("wat");
initInteractiveEditor();
execute.addEventListener("click", () => {
output.textContent = "";
applyCode();
});
reset.addEventListener("click", () => window.location.reload());
} else {
console.warn(
`Feature ${
"WebAssembly" in window ? exampleFeature : "WebAssembly"
} is not supported; code editor disabled.`,
);
}
})();