PageRenderTime 54ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/js/pycloud.js

https://github.com/siong1987/PyCloud
JavaScript | 559 lines | 454 code | 72 blank | 33 comment | 72 complexity | 9c17eeae77ed95dabb160de0d8910a8d MD5 | raw file
  1. jQuery.noConflict();
  2. var editor;
  3. var storage;
  4. var currentFile;
  5. var currentContent;
  6. // ---------------- Storage class ----------------
  7. // This is a wrapper for HTML5's localStorage. It lets us use file paradigm
  8. // instead of key-value pair.
  9. var Storage = function() {
  10. var filePrefix = 'file_';
  11. function keyForFilename(filename) {
  12. return filePrefix + filename;
  13. }
  14. this.files = [];
  15. var len = localStorage.length;
  16. for (var i = 0; i < len; i++) {
  17. var key = localStorage.key(i);
  18. if (key.indexOf(filePrefix) == 0) {
  19. var prefixLen = filePrefix.length;
  20. var filename = key.substr(prefixLen, key.length - prefixLen);
  21. var file = new File(filename);
  22. file.storage = this;
  23. this.files.push(file);
  24. }
  25. }
  26. this.addFile = function(file) {
  27. var key = keyForFilename(file.name);
  28. localStorage.setItem(key, JSON.stringify(file.content));
  29. file.storage = this;
  30. this.files.push(file);
  31. }
  32. this.updateFile = function(file) {
  33. var key = keyForFilename(file.name);
  34. localStorage.setItem(key, JSON.stringify(file.content));
  35. }
  36. this.fillContent = function(file) {
  37. var key = keyForFilename(file.name);
  38. file.content = JSON.parse(localStorage.getItem(key));
  39. }
  40. this.hasFilename = function(filename) {
  41. var key = keyForFilename(filename);
  42. return localStorage.getItem(key) !== null;
  43. }
  44. this.getFile = function(filename) {
  45. for (var i = 0; i < this.files.length; i++) {
  46. if (this.files[i].name == filename)
  47. return this.files[i];
  48. }
  49. return null;
  50. }
  51. this.removeFile = function(file) {
  52. var key = keyForFilename(file.name);
  53. localStorage.removeItem(key);
  54. for (var i = 0; i < this.files.length; i++) {
  55. if (this.files[i].name == file.name) {
  56. this.files.splice(i, 1);
  57. break;
  58. }
  59. }
  60. }
  61. }
  62. // ---------------- File class ----------------
  63. // A file consists of name, content, and storage. Storage is where the file belongs to.
  64. // It's possible that a file may not have a storage, such as files that are newly created
  65. // and haven't been saved yet, e.g. "untitled.py".
  66. var File = function(filename) {
  67. this.name = filename;
  68. this.content = {'text':''};
  69. this.loadedContent = false;
  70. this.storage = null;
  71. this.getContent = function() {
  72. if (!this.loadedContent) {
  73. this.storage.fillContent(this);
  74. this.loadedContent = true;
  75. }
  76. return this.content;
  77. }
  78. this.setContent = function(content) {
  79. this.content = content;
  80. this.loadedContent = true;
  81. }
  82. this.save = function() {
  83. if (this.storage) {
  84. this.storage.updateFile(this);
  85. }
  86. };
  87. this.saveAs = function(filename, defaultStorage) {
  88. if (this.storage) {
  89. if (!this.storage.hasFilename(filename)) {
  90. file = new File(filename);
  91. file.setContent(this.content);
  92. this.storage.addFile(file);
  93. return file;
  94. } else {
  95. alert("File named " + filename + " already existed.");
  96. }
  97. } else {
  98. if (!defaultStorage.hasFilename(filename)) {
  99. this.name = filename;
  100. defaultStorage.addFile(this);
  101. return null;
  102. } else {
  103. alert("File named " + filename + " already existed.");
  104. }
  105. }
  106. return null;
  107. };
  108. this.remove = function() {
  109. if (this.storage) {
  110. this.storage.removeFile(this);
  111. } else {
  112. this.storage = 1; // hacky, to make the file browser remove it
  113. }
  114. }
  115. };
  116. // ---------------- File Browser ----------------
  117. // The file browser shows a list of files. It maintains a list of storages and a list of files that
  118. // are not in any storage (tempFiles). It is possible that a file that was once in the tempFiles
  119. // may move to a storage. (For example, we may save "untitled.py" to a storage.)
  120. // In that case, we remove it from the tempFiles list.
  121. var browser = new function() {
  122. this.storages = [];
  123. this.tempFiles = [];
  124. this.files = [];
  125. this.addStorage = function(storage) {
  126. this.storages.push(storage);
  127. }
  128. this.addTempFile = function(file) {
  129. this.tempFiles.push(file);
  130. };
  131. this.removeTempFile = function(file) {
  132. for (var i = 0; i < this.tempFiles.length; i++) {
  133. if (this.tempFiles[i].name == file.name) {
  134. this.tempFiles.splice(i, 1);
  135. break;
  136. }
  137. }
  138. }
  139. var sortFunction = function(x, y) {
  140. var a = x.name.toLowerCase(), b = y.name.toLowerCase();
  141. if (a > b) return 1;
  142. if (a < b) return -1;
  143. return 0;
  144. };
  145. this.reload = function() {
  146. var files = [];
  147. // add files that are in storages
  148. for (var i = 0; i < this.storages.length; i++)
  149. files = files.concat(this.storages[i].files);
  150. // add files that are not in storage (if it is in a storage, we remove it from the list)
  151. for (var i = this.tempFiles.length-1; i >= 0; i--) {
  152. var file = this.tempFiles[i];
  153. if (file.storage) {
  154. this.tempFiles.splice(i, 1);
  155. } else {
  156. files.push(file);
  157. }
  158. }
  159. // sort alphabetically
  160. files = files.sort(sortFunction);
  161. this.files = files;
  162. var tmp = "<ul>";
  163. for (var i = 0; i < files.length; i++) {
  164. var cls = "";
  165. if (files[i] == currentFile)
  166. cls = "class='selected' ";
  167. tmp += "<li " + cls +
  168. "onclick=\"showFile(browser.files[" + i + "])\" " +
  169. "oncontextmenu=\"showContextMenu(event, browser.files[" + i + "])\">" +
  170. "<img src='img/document.png'> " +
  171. files[i].name + "</li>";
  172. }
  173. tmp += "</ul>";
  174. document.getElementById("browser").innerHTML = tmp;
  175. };
  176. }
  177. // ---------------- Guide Box ----------------
  178. var guide = new function() {
  179. var shows = false;
  180. this.setText = function(text) {
  181. document.getElementById("guide").innerHTML = text;
  182. }
  183. this.show = function() {
  184. if (!shows) {
  185. shows = true;
  186. document.getElementById("guide-outer").style.display = 'block';
  187. document.getElementById("editor").style.right = '300px';
  188. }
  189. }
  190. this.hide = function() {
  191. if (shows) {
  192. shows = false;
  193. document.getElementById("guide-outer").style.display = 'none';
  194. document.getElementById("editor").style.right = '0';
  195. }
  196. }
  197. };
  198. // ---------------- Console ----------------
  199. var console = new function() {
  200. this.setText = function(text) {
  201. text = text.replace(/ /g, '&nbsp;').replace(/\n/g, '<br>');
  202. document.getElementById("console").innerHTML = text;
  203. }
  204. }
  205. // ---------------- Utilities ----------------
  206. function updateCurrentContent() {
  207. currentContent.text = editor.getSession().getValue();
  208. }
  209. // ---------------- main menu actions ----------------
  210. function run() {
  211. var text = editor.getSession().getValue();
  212. var originalText = text;
  213. if (currentContent.inject) {
  214. text = currentContent.inject + text;
  215. }
  216. var output = python.execute(text);
  217. console.setText(output);
  218. editor.focus();
  219. if (currentContent.callback) {
  220. f = eval(currentContent.callback);
  221. if (f(originalText, output)) {
  222. currentContent.state = 1;
  223. } else {
  224. currentContent.state = 0;
  225. }
  226. reloadGuide();
  227. }
  228. if (currentFile.storage)
  229. save();
  230. }
  231. var newFileCounter = 0;
  232. function createNewFile() {
  233. newFileCounter++;
  234. var filename = newFileCounter > 1 ? ("untitled-" + newFileCounter + ".py") : "untitled.py";
  235. var file = new File(filename);
  236. file.setContent({'text':''});
  237. browser.addTempFile(file);
  238. showFile(file);
  239. }
  240. function save() {
  241. if (currentFile.storage) {
  242. updateCurrentContent();
  243. currentFile.setContent(currentContent);
  244. currentFile.save();
  245. } else {
  246. saveAs();
  247. }
  248. }
  249. function saveAs() {
  250. updateCurrentContent();
  251. currentFile.setContent(currentContent);
  252. var filename = prompt("Save as:", ".py");
  253. if (filename) {
  254. newFile = currentFile.saveAs(filename, storage);
  255. if (newFile)
  256. currentFile = newFile;
  257. browser.reload();
  258. }
  259. }
  260. function clearLocalStorage() {
  261. localStorage.clear();
  262. window.location = self.location;
  263. }
  264. function acknowledgements() {
  265. window.open("http://www.jitouch.com/pycloud/acknowledgements.txt");
  266. }
  267. // ---------------- context menu actions ----------------
  268. var contextMenuFile;
  269. function deleteFile() {
  270. if (confirm("Are you sure you want to delete " + contextMenuFile.name + "?")) {
  271. contextMenuFile.remove();
  272. browser.reload();
  273. if (currentFile == contextMenuFile) {
  274. if (browser.files.length > 0)
  275. showFile(browser.files[0]);
  276. else
  277. createNewFile();
  278. }
  279. }
  280. }
  281. function showContextMenu(e, file) {
  282. contextMenuFile = file;
  283. jQuery('#contextmenu').offset({'top': e.clientY, 'left': e.clientX});
  284. jQuery('#contextmenu li').trigger('click');
  285. e.preventDefault();
  286. }
  287. // ---------------- other stuffs ----------------
  288. function showFile(file) {
  289. if (currentFile) {
  290. updateCurrentContent();
  291. currentFile.setContent(currentContent);
  292. }
  293. currentFile = file;
  294. currentContent = file.getContent();
  295. editor.getSession().setValue(currentContent.text);
  296. browser.reload();
  297. if (currentContent.guide) {
  298. guide.show();
  299. reloadGuide();
  300. } else {
  301. guide.hide();
  302. }
  303. }
  304. function reloadGuide() {
  305. var guideText = currentContent.guide;
  306. if (currentContent.state !== undefined) {
  307. if (currentContent.state == 1) {
  308. guideText += "<br><br>" +
  309. "<font color=blue>" + currentContent.right + "</font>";
  310. if (currentContent.next) {
  311. guideText += "<br><br>" + "Go to the " +
  312. "<a href=\"javascript:showFile(storage.getFile('" + currentContent.next + "'))\">next lesson</a>.";
  313. }
  314. } else if (currentContent.state == 0) {
  315. guideText += "<br><br>" +
  316. "<font color=red>" + currentContent.wrong + "</font>";
  317. }
  318. }
  319. guide.setText(guideText);
  320. }
  321. function createDefaultFiles() {
  322. if (localStorage.getItem("created-default-files") == null) {
  323. localStorage.setItem("created-default-files", "1");
  324. for (var i = 0; i < lessons.length; i++) {
  325. var file = new File(lessons[i].name);
  326. file.setContent(lessons[i].content);
  327. storage.addFile(file);
  328. }
  329. localStorage.setItem("recent-file", lessons[0].name);
  330. browser.reload();
  331. }
  332. }
  333. function setupEditor() {
  334. editor = ace.edit("editor");
  335. editor.setTheme("ace/theme/textmate");
  336. editor.setShowPrintMargin(false);
  337. editor.getSession().setUseSoftTabs(false);
  338. var PythonMode = require("ace/mode/python").Mode;
  339. editor.getSession().setMode(new PythonMode());
  340. var canon = require('pilot/canon')
  341. /*
  342. canon.addCommand({
  343. name: 'run',
  344. bindKey: {win: 'Ctrl-R', mac: 'Command-R', sender: 'editor'},
  345. exec: function() {run();}
  346. });
  347. */
  348. canon.addCommand({
  349. name: 'new',
  350. bindKey: {win: 'Ctrl-N', mac: 'Command-N', sender: 'editor'},
  351. exec: function() {createNewFile();}
  352. });
  353. canon.addCommand({
  354. name: 'save',
  355. bindKey: {win: 'Ctrl-S', mac: 'Command-S', sender: 'editor'},
  356. exec: function() {save();}
  357. });
  358. }
  359. function setupEditorForMobileDevices() {
  360. var $ = jQuery;
  361. editor = new function() {
  362. this.value = "";
  363. this.session = new function() {
  364. this.getValue = function() {
  365. editor.value = document.getElementById("editor").value;
  366. return editor.value;
  367. };
  368. this.setValue = function(value) {
  369. value = value.replace(/\t/g, ' ')
  370. editor.value = value;
  371. document.getElementById("editor").value = value;
  372. };
  373. };
  374. this.getSession = function() {
  375. return this.session;
  376. };
  377. this.focus = function() {};
  378. this.undo = function() {};
  379. this.redo = function() {};
  380. this.find = function() {};
  381. };
  382. $("#editor").replaceWith("<textarea id='editor' class='textarea' autocorrect='off' autocapitalize='off'></textarea>");
  383. }
  384. function setupMenus() {
  385. var $ = jQuery;
  386. var commands = {
  387. run: run,
  388. newFile: createNewFile,
  389. save: save,
  390. saveAs: saveAs,
  391. clearLocalStorage: clearLocalStorage,
  392. deleteFile: deleteFile,
  393. undo: function() {editor.undo();},
  394. redo: function() {editor.redo();},
  395. find: function() {var needle = prompt("Find:"); editor.find(needle);},
  396. findNext: function() {editor.findNext();},
  397. goToLine: function() {
  398. var line = parseInt(prompt("Enter line number:"));
  399. if (!isNaN(line))
  400. editor.gotoLine(line);
  401. },
  402. toggleLineNumbers: function() {
  403. editor.renderer.setShowGutter(!editor.renderer.getShowGutter());
  404. },
  405. acknowledgements: acknowledgements
  406. };
  407. $('#menu').clickMenu({
  408. onClick: function() {
  409. var a = $(this).find('>a');
  410. if (a.length) {
  411. href = a.attr('href');
  412. commands[href]();
  413. $('#menu').trigger('closemenu');
  414. }
  415. return false;
  416. }
  417. });
  418. $('#contextmenu').clickMenu({
  419. onClick: function() {
  420. var a = $(this).find('>a');
  421. if (a.length) {
  422. href = a.attr('href');
  423. commands[href]();
  424. $('#contextmenu').trigger('closemenu');
  425. }
  426. return false;
  427. }
  428. });
  429. }
  430. function setupControl() {
  431. var $ = jQuery;
  432. $('#control-run').click(run);
  433. $('#control-run').mousedown(function() {
  434. $(this).addClass("pushed");
  435. });
  436. $('#control-run').mouseup(function() {
  437. $(this).removeClass("pushed");
  438. });
  439. $('#control-redo').click(function() {
  440. editor.redo();
  441. });
  442. $('#control-redo').mousedown(function() {
  443. $(this).addClass("pushed");
  444. });
  445. $('#control-redo').mouseup(function() {
  446. $(this).removeClass("pushed");
  447. });
  448. $('#control-undo').click(function() {
  449. editor.undo();
  450. });
  451. $('#control-undo').mousedown(function() {
  452. $(this).addClass("pushed");
  453. });
  454. $('#control-undo').mouseup(function() {
  455. $(this).removeClass("pushed");
  456. });
  457. }
  458. jQuery(document).ready(function($){
  459. // set up editor
  460. if (navigator.userAgent.match(/like Mac OS X/i) || navigator.userAgent.indexOf("ndroid") != -1) {
  461. setupEditorForMobileDevices();
  462. } else {
  463. setupEditor();
  464. }
  465. // set up main menu & context menu
  466. setupMenus();
  467. // set up control bar
  468. setupControl();
  469. // set up file browser & storage
  470. storage = new Storage();
  471. browser.addStorage(storage);
  472. createDefaultFiles();
  473. if (storage.files.length == 0) {
  474. createNewFile();
  475. } else {
  476. var recentFilename = localStorage.getItem('recent-file');
  477. if (recentFilename && storage.hasFilename(recentFilename)) {
  478. showFile(storage.getFile(recentFilename));
  479. } else {
  480. showFile(storage.files[0]);
  481. }
  482. }
  483. });
  484. window.onbeforeunload = function() {
  485. localStorage.setItem('recent-file', currentFile.name);
  486. //TODO: warn user to save files
  487. }