forked from cs50/harvard.cs50.browser
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbrowser.js
More file actions
518 lines (418 loc) · 17.3 KB
/
browser.js
File metadata and controls
518 lines (418 loc) · 17.3 KB
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
define(function(require, exports, module) {
main.consumes = [
"c9", "commands", "dialog.error", "Editor", "editors", "fs", "layout",
"MenuItem", "menus", "tabManager", "proc", "settings", "tree", "ui"
];
main.provides = ["harvard.cs50.browser"];
return main;
function main(options, imports, register) {
var c9 = imports.c9;
var commands = imports.commands;
var Editor = imports.Editor;
var editors = imports.editors;
var fs = imports.fs;
var layout = imports.layout;
var MenuItem = imports.MenuItem;
var menus = imports.menus;
var proc = imports.proc;
var showError = imports["dialog.error"].show;
var tabs = imports.tabManager;
var settings = imports.settings;
var tree = imports.tree;
var ui = imports.ui;
var _ = require("lodash");
var basename = require("path").basename;
var extname = require("path").extname;
var join = require("path").join;
var BROWSER_VER = 1;
var extensions = ["db", "db3", "sqlite", "sqlite3"];
var handle = editors.register("browser", "Browser", Browser, extensions);
var cssInserted = false;
handle.insertCss = function() {
// ensure CSS is inserted only once
if (cssInserted)
return;
cssInserted = true;
ui.insertCss(require("text!./style.css"), handle);
}
/**
* adds Reload to tab context menu
*/
handle.addReloadItem = function() {
// add "Reload" item once
if (handle.reloadAdded)
return;
// context menu of tab button
handle.tabMenu = menus.get("context/tabs").menu;
if (!handle.tabMenu)
return;
// create "Reload" item
handle.reloadItem = new MenuItem({
caption: "Reload",
onclick: function() {
var tab = tabs.focussedTab;
if (tab.editorType === "browser")
tab.editor.reloadTab(tab);
},
visible: false
});
// add "Reload" item to context menu
menus.addItemByPath("context/tabs/Reload", handle.reloadItem, 0, handle);
handle.reloadAdded = true;
// show "Reload" item only if tab is browser
handle.tabMenu.on("prop.visible", function(e) {
if (tabs.focussedTab.editorType === "browser" && e.value)
handle.reloadItem.show();
else
handle.reloadItem.hide();
});
};
/**
* Toggles loading spinner
*/
handle.toggleLoadingSpinner = function(container, tab, visible) {
if (visible) {
tab.classList.add("loading");
container.classList.add("cs50-browser-loading");
}
else {
tab.classList.remove("loading");
container.classList.remove("cs50-browser-loading");
}
};
/**
* opens and reuses a Cloud9 tab for browser editor
*/
function openBrowserTab(options, onClose) {
tabs.open({
name: options.name || "browser-tab",
document: {
title: options.title || "browser",
browser: {
content: options.content,
path: options.path
}
},
editorType: "browser",
active: true,
focus: true,
}, onClose || function() {});
}
// add c9 exec browser
commands.addCommand({
name: "browser",
exec: function(args) {
if (!_.isArray(args) || args.length !== 2 || !_.isString(args[1]))
return console.error("Usage: c9 exec browser path");
// open phpliteadmin tab for database files
if (extensions.indexOf(extname(args[1]).substring(1)) > -1) {
// join cwd and path only if path is relative
var path = args[1].startsWith("/")
? args[1]
: join(args[0], args[1]);
return openBrowserTab({
name: "phpliteadmin-tab",
title: "phpliteadmin",
path: path
}, handleTabClose);
}
// open SPL programs in built-in browser tab
fs.readFile(args[1], function(err, data) {
if (err)
throw err;
// remove shebang
data = data.replace(/^#!\/usr\/bin\/env browser\s*$/m, "");
openBrowserTab({
title: basename(args[1]),
content: data
});
});
}
}, handle);
// write ~/bin/browser to use in shebang
var browserPath = "~/bin/browser";
fs.exists(browserPath, function(exists) {
var ver = settings.getNumber("project/cs50/simple/@browser");
if (!exists || isNaN(ver) || ver < BROWSER_VER) {
fs.writeFile(browserPath, require("text!./bin/browser"), function(err) {
if (err)
throw err;
fs.chmod(browserPath, 755, function(err) {
if (err)
throw err;
settings.set("project/cs50/simple/@browser", BROWSER_VER);
});
});
}
});
register(null, {
"harvard.cs50.browser": handle
});
/**
* Opens files selected in file browser, ensuring browser
* (re)uses a single tab
*/
function openSelection(opts) {
if (!c9.has(c9.STORAGE) || !tree.tree)
return;
var sel = tree.tree.selection.getSelectedNodes();
var db = null;
// get last selected db file, deselecting all db files temporarily
sel.forEach(function(node) {
if (node && node.path && extensions.indexOf(extname(node.path).substring(1)) > -1) {
db = node;
tree.tree.selection.unselectNode(db);
}
});
// open non-db selected files (if any)
if (sel.length > 0)
tree.openSelection(opts);
// open last selected db file, selecting it back
if (db) {
// just focus tab if phpliteadmin is running same db
var tab = tabs.findTab("phpliteadmin-tab");
if (tab && tab.document.lastState.browser.path === db.path)
return tabs.focusTab(tab);
openBrowserTab({
name: "phpliteadmin-tab",
title: "phpliteadmin",
path: db.path.replace(/^\//, c9.workspaceDir + "/")
}, handleTabClose);
tree.tree.selection.selectNode(db, true);
}
}
/**
* Hooks event handler to kill phpliteadmin process associated with the
* document currently open in tab, when tab is closed.
*/
function handleTabClose(err, tab) {
if (err)
return console.error(err);
// ensure handler hooked once
tab.off("close", handleTabClose);
// kill phpliteadmin when tab is closed
tab.on("close", function() {
var pid = tab.document.lastState.browser.pid;
// process.kill isn't available after reload (bug?)
if (pid)
proc.spawn("kill", { args: ["-1", pid ]}, function() {});
});
}
/**
* Spawns phpliteadmin and calls callback, passing in url, or error
*
* @param {string} path path of db file
*/
function startPhpliteadmin(path, callback) {
if (!path)
return;
// spawn phpliteadmin
proc.spawn("phpliteadmin", {
args: [ "--url-only", path ] },
function(err, process) {
if (err)
return callback(err);
// keep running after reload
process.unref();
// get phpliteadmin url
var data = "";
process.stdout.on("data", function handleOutput(chunk) {
data += chunk;
var matches = data.match(/(https?:\/\/.+)\s/);
if (matches && matches[1]) {
process.stdout.off("data", handleOutput);
callback(null, matches[1], process.pid);
}
});
});
}
// hook new handler for Open to open db files
tree.once("draw", function() {
if (tree.tree) {
tree.tree.off("afterChoose", tree.openSelection);
tree.tree.on("afterChoose", openSelection);
}
});
function Browser(){
var plugin = new Editor("CS50", main.consumes, extensions);
var emit = plugin.getEmitter();
var container, iframe;
var currDoc, currSession;
var timeout;
// draw editor
plugin.on("draw", function(e) {
// insert css
handle.insertCss();
// add "Reload" menu item to tab button context menu
handle.addReloadItem();
// create and style iframe
iframe = document.createElement("iframe");
iframe.style.background = "white";
iframe.style.borderWidth = "0";
iframe.style.display = "none";
iframe.style.width = iframe.style.height = "100%";
// remember container
container = e.htmlNode;
// append iframe
container.appendChild(iframe);
});
/**
* Reloads current built-in browser tab
*/
function reloadTab(tab) {
if (tab === currDoc.tab) {
// iframe.contentWindow.location.reload violates same-origin
if (currSession.url)
updateIframe({ url: iframe.src });
else if (currSession.content)
updateIframe({ content: currSession.content });
}
}
function updateIframe(options) {
// reset onload handler
iframe.onload = function() {};
// reset iframe src
iframe.src = "about:blank";
// hide iframe
iframe.style.display = "none";
if (!options)
return;
// show loading spinner
handle.toggleLoadingSpinner(container, currDoc.tab, true);
// if url provided
if (options.url) {
currSession.url = options.url;
iframe.src = options.url;
}
iframe.onload = function () {
// avoid triggering this infinitely
iframe.onload = function() {};
// if SPL program
if (options.content) {
currSession.content = options.content;
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(options.content);
iframe.contentWindow.document.close();
}
// show iframe back
iframe.style.display = "initial";
// hide loading spinner
handle.toggleLoadingSpinner(container, currDoc.tab, false);
}
}
plugin.on("documentLoad", function(e) {
// reset iframe
updateIframe();
// set current document and session
currDoc = e.doc;
currSession = currDoc.getSession();
// when content should be written to iframe
plugin.on("contentSet", function(content) {
updateIframe({ content: content })
});
/**
* Toggles editor's theme based on current skin.
*/
function setTheme(e) {
if (!currDoc)
return;
// get document's tab
var tab = currDoc.tab;
// handle dark themes
if (e.theme.indexOf("dark") > -1) {
// change tab-button colors
container.style.backgroundColor = tab.backgroundColor = "#303130";
container.classList.add("dark");
tab.classList.add("dark");
}
// handle light themes
else {
// change tab-button colors
container.style.backgroundColor = tab.backgroundColor = "#f1f1f1";
container.classList.remove("dark");
tab.classList.remove("dark");
}
}
// toggle editor's theme when theme changes
layout.on("themeChange", setTheme, currSession);
// set editor's theme initially
setTheme({ theme: settings.get("user/general/@skin") });
});
plugin.on("documentActivate", function(e) {
// set current document and session
currDoc = e.doc;
currSession = currDoc.getSession();
if (currSession.url && currSession.url !== iframe.src)
updateIframe({ url: currSession.url });
else if (currSession.content)
updateIframe({ content: currSession.content });
});
// when path changes
plugin.on("setState", function(e) {
function handler(err, url, pid) {
if (err)
return console.error(err);
// set or update session's url
currSession.url = url;
// set or update phpliteadmin pid
currSession.pid = pid;
// give chance to server to start
timeout = setTimeout(function() {
// reset iframe
updateIframe({ url: url });
}, 1000);
}
// reset and hide iframe
updateIframe();
// update current document and session
currDoc = e.doc;
currSession = currDoc.getSession();
// set or update current db path
currSession.path = e.state.path;
// set or update current phpliteadmin pid
if (e.state.pid) {
currSession.pid = e.state.pid;
handleTabClose(null, currDoc.tab);
}
// if phpliteadmin is already running, use url
if (e.state.url) {
// restart phpliteadmin process if no longer running
// process is killed after workspace is restarted
proc.execFile("kill", { args: ["-0", currSession.pid]}, (err) => {
if (err)
return startPhpliteadmin(currSession.path, handler);
currSession.url = e.state.url;
updateIframe({ url: currSession.url });
});
}
// handle SDL programs
else if (e.state.content) {
currSession.content = e.state.content;
emit("contentSet", currSession.content);
}
// handle database files
else {
// show loading spinner
handle.toggleLoadingSpinner(container, currDoc.tab, true);
// refrain from updating iframe if we're starting another phpliteadmin
clearTimeout(timeout);
updateIframe();
// start phpliteadmin
startPhpliteadmin(currSession.path, handler);
}
});
// remember state between reloads
plugin.on("getState", function(e) {
e.state.content = e.doc.getSession().content;
e.state.path = e.doc.getSession().path;
e.state.pid = e.doc.getSession().pid;
e.state.url = e.doc.getSession().url;
});
plugin.freezePublicAPI({
reloadTab: reloadTab
});
plugin.load(null, "harvard.cs50.browser");
return plugin;
}
}
});