Source: singletons/workspace.js

  1. Syntree.config_maps.workspace = {};
  2. Syntree.config_maps.workspace.accept_unmapped_config = false;
  3. Syntree.config_maps.workspace.map = {
  4. /**
  5. * Is the tutorial enabled?
  6. *
  7. * @type {boolean}
  8. *
  9. * @see Syntree.Tutorial
  10. *
  11. * @memberof Syntree.Workspace
  12. */
  13. tutorial_enabled: {
  14. require: 'boolean',
  15. default_value: false,
  16. },
  17. /**
  18. * Is uploading enabled?
  19. *
  20. * @type {boolean}
  21. *
  22. * @see Syntree.Tutorial
  23. *
  24. * @memberof Syntree.Workspace
  25. */
  26. upload_enabled: {
  27. require: 'boolean',
  28. default_value: true,
  29. },
  30. /**
  31. * Path to a PHP script for exporting a tree.
  32. * Script should return a download link for an export file on success, or false on failure.
  33. *
  34. * @type {string}
  35. *
  36. * @see Syntree.Workspace._eventExprt
  37. *
  38. * @memberof Syntree.Workspace
  39. */
  40. export_tree_script: {
  41. require: 'string',
  42. default_value: '#undefined',
  43. },
  44. /**
  45. * Is focus checking enabled?
  46. * Focus checking is for if the app is embedded within a larger page.
  47. * It prevents confusion about whether or not the app has focus.
  48. *
  49. * @type {boolean}
  50. *
  51. * @memberof Syntree.Workspace
  52. */
  53. focus_checking_enabled: {
  54. require: 'boolean',
  55. default_value: false,
  56. },
  57. };
  58. /**
  59. * @class
  60. * @classdesc Workspace is in charge of taking user input, sanitizing it, and sending it to the appropriate lower-level control structure.
  61. */
  62. Syntree.Workspace = {
  63. initialize: function(config_matrix) {
  64. Syntree.Lib.config(config_matrix, this);
  65. // Make changes to environment based on initialization parameters.
  66. // Tutorial.
  67. if (this.tutorial_enabled) {
  68. $(document)
  69. .ready(
  70. function() {
  71. modal_open('tutorial');
  72. })
  73. .on(
  74. 'click',
  75. '.button_modal__begin-tutorial',
  76. function() {
  77. Syntree.Tutorial.start();
  78. })
  79. .on(
  80. 'click',
  81. '.toolbar_button__tutorial',
  82. function() {
  83. Syntree.Workspace._eventRewatchTutorial();
  84. }
  85. );
  86. } else {
  87. $('.toolbar_button__tutorial').remove();
  88. }
  89. // Export functionality.
  90. if (Syntree.Lib.checkType(this.export_tree_script, 'undefined')) {
  91. $('.toolbar_button__export').remove();
  92. $('.modal_export').remove();
  93. }
  94. // Upload functionality.
  95. if (!this.upload_enabled) {
  96. $('.toolbar_button__upload').remove();
  97. }
  98. // Focus checking.
  99. if (this.focus_checking_enabled) {
  100. $('#workspace_container').prepend('<div class="focus_check_overlay"></div>');
  101. $('body').prepend('<div class="focus_check_underlay"></div>');
  102. $('.focus_check_underlay').hide();
  103. this.focused = false;
  104. }
  105. this._attachEventListeners();
  106. this.page = new Syntree.Page();
  107. this.page.addTree(); // Adds the default tree.
  108. this.page.select(this.page.tree.getRoot());
  109. },
  110. /**
  111. * Attach various event listeners needed for processing user input. This function is a convenience only, used so that [initialize()]{@link Workspace.initialize}) is a bit less cluttered.
  112. */
  113. _attachEventListeners: function() {
  114. // This stuff is to fix dragging, which as default triggers on right click.
  115. // We want it to NOT trigger on right click, so we maintain Workspace.rightClick
  116. // for the drag functions to check.
  117. $(document)
  118. .on(
  119. 'mousedown',
  120. function(e) {
  121. if (e.which === 3) {
  122. Syntree.Workspace.rightClick = true;
  123. };
  124. })
  125. .on(
  126. 'mouseup',
  127. function(e) {
  128. if (e.which === 3) {
  129. Syntree.Workspace.rightClick = false;
  130. };
  131. }
  132. );
  133. window.onblur = function() {
  134. Syntree.Workspace.rightClick = false;
  135. }
  136. // --------------------------------------------------------------------------
  137. // Basic events, funneled to event functions below
  138. $(document)
  139. .on(
  140. 'click',
  141. '.arrow, .arrow-shadow',
  142. function(e) {
  143. Syntree.Workspace._eventArrowClick(e);
  144. })
  145. .on(
  146. 'click',
  147. '.branch, .branch-shadow, .triangle',
  148. function(e) {
  149. Syntree.Workspace._eventBranchClick(e);
  150. })
  151. .on(
  152. 'click',
  153. '.triangle-button',
  154. function(e) {
  155. Syntree.Workspace._eventTriangleButtonClick(e);
  156. })
  157. .on(
  158. 'click',
  159. '.node-label',
  160. function(e) {
  161. Syntree.Workspace._eventNodeClick(e);
  162. })
  163. .on(
  164. 'click',
  165. '.delete_button',
  166. function() {
  167. Syntree.Workspace._eventDel();
  168. })
  169. .on(
  170. 'click',
  171. '#page-background',
  172. function(e) {
  173. Syntree.Workspace._eventBGClick(e);
  174. })
  175. .on(
  176. 'dblclick',
  177. '.node-label, .highlight',
  178. function() {
  179. Syntree.Workspace._eventEnter();
  180. })
  181. .on(
  182. 'input',
  183. '.editor',
  184. function() {
  185. Syntree.Workspace._eventEditorTyping();
  186. })
  187. .on(
  188. 'keydown',
  189. function(e) {
  190. if ((Syntree.Workspace.focus_checking_enabled && Syntree.Workspace.focused) ||
  191. !Syntree.Workspace.focus_checking_enabled) {
  192. if (e.keyCode === 13) { // Enter
  193. Syntree.Workspace._eventEnter();
  194. } else if (e.keyCode === 37) { // Left arrow key
  195. Syntree.Workspace._eventLeft(e);
  196. } else if (e.keyCode === 38) { // Up arrow key
  197. Syntree.Workspace._eventUp();
  198. return false;
  199. } else if (e.keyCode === 39) { // Right arrow key
  200. Syntree.Workspace._eventRight(e);
  201. } else if (e.keyCode === 40) { // Down arrow key
  202. Syntree.Workspace._eventDown(e);
  203. return false;
  204. } else if (e.keyCode === 46) { // Delete key
  205. Syntree.Workspace._eventDel();
  206. } else if (e.keyCode === 27) { // Esc key
  207. Syntree.Workspace._eventEsc();
  208. } else if (e.keyCode === 90 && e.ctrlKey) { // CTRL + Z
  209. Syntree.Workspace._eventUndo();
  210. }
  211. }
  212. }
  213. );
  214. // Focus checking.
  215. if (Syntree.Workspace.focus_checking_enabled) {
  216. $(document)
  217. .on(
  218. 'click',
  219. '.focus_check_overlay',
  220. function() {
  221. Syntree.Workspace._eventFocus();
  222. })
  223. .on(
  224. 'click',
  225. '.focus_check_underlay',
  226. function() {
  227. Syntree.Workspace._eventUnfocus()
  228. }
  229. );
  230. }
  231. // Exporting trees.
  232. if (Syntree.Lib.checkType(this.export_tree_script, 'string')) {
  233. $(document)
  234. .on(
  235. 'click',
  236. '.modal_section__filetype .modal_label',
  237. function(e) {
  238. Syntree.Workspace._eventFiletypeLabelClick(e);
  239. })
  240. .on(
  241. 'click',
  242. '.button_modal__export',
  243. function() {
  244. $(this).addClass('loading');
  245. var type = $('.modal_section__filetype input:checked').val();
  246. if (type === 'bracket-file') {
  247. Syntree.Workspace._eventExportBrackets();
  248. } else if (type === 'tree-file') {
  249. Syntree.Workspace._eventExportTreeFile();
  250. } else if (type === 'png') {
  251. Syntree.Workspace._eventExportImage();
  252. }
  253. $(this).removeClass('loading');
  254. }
  255. );
  256. }
  257. // Uploading trees.
  258. if (this.upload_enabled) {
  259. $(document).on(
  260. 'click',
  261. '.toolbar_button__upload',
  262. function() {
  263. Syntree.Workspace._eventUpload();
  264. });
  265. }
  266. // Saving trees.
  267. if (Syntree.Lib.checkType(this.save_tree_script, 'string')) {
  268. $(document).on(
  269. 'click',
  270. '.toolbar_button__save',
  271. function(){
  272. Syntree.Workspace._eventSave();
  273. });
  274. }
  275. // Opening trees.
  276. if (Syntree.Lib.checkType(this.get_trees_script, 'string')) {
  277. $(document).on(
  278. 'click',
  279. '.toolbar_button__open',
  280. function() {
  281. $.post(Syntree.Workspace.get_trees_script, {}, function(result) {
  282. $('.modal_section__trees').html(result);
  283. });
  284. });
  285. }
  286. },
  287. /**
  288. * Code to run when a branch's triangle button is clicked.
  289. *
  290. * @see Syntree.Branch
  291. */
  292. _eventTriangleButtonClick: function(e) {
  293. var clicked = e.currentTarget;
  294. var clickedId = $(clicked).attr('id');
  295. var id = Number(clickedId.substr(clickedId.lastIndexOf('-') + 1, clickedId.length));
  296. Syntree.Workspace.page.allElements[id].triangleToggle();
  297. },
  298. /**
  299. * Code to run when a branch is clicked.
  300. *
  301. * @see Syntree.Branch
  302. * @see Syntree.Page.select
  303. */
  304. _eventBranchClick: function(e) {
  305. var clicked = e.currentTarget;
  306. var clickedId = $(clicked).attr('id');
  307. var id = Number(clickedId.substr(clickedId.lastIndexOf('-') + 1, clickedId.length));
  308. Syntree.Workspace.page.select(Syntree.Workspace.page.allElements[id]);
  309. },
  310. /**
  311. * Code to run when an arrow is clicked.
  312. *
  313. * @see Syntree.Arrow
  314. * @see Syntree.Page.select
  315. */
  316. _eventArrowClick: function(e) {
  317. var clicked = e.currentTarget;
  318. var clickedId = $(clicked).attr('id');
  319. var id = Number(clickedId.substr(clickedId.lastIndexOf('-') + 1, clickedId.length));
  320. Syntree.Workspace.page.select(Syntree.Workspace.page.allElements[id]);
  321. },
  322. /**
  323. * Code to run when a user requests a tutorial restart/rewatch.
  324. *
  325. * @see Syntree.Tutorial
  326. */
  327. _eventRewatchTutorial: function() {
  328. var check;
  329. if (Syntree.Tutorial.running) {
  330. check = confirm('Restart tutorial?');
  331. } else {
  332. check = confirm('This will delete any work you have open. Start tutorial anyway?');
  333. }
  334. if (check) {
  335. Syntree.Workspace.page.tree.delete();
  336. Syntree.Workspace.page.addTree();
  337. Syntree.Workspace.page.select(Syntree.Workspace.page.tree.getRoot());
  338. Syntree.Tutorial.start();
  339. }
  340. },
  341. /**
  342. * Code to run when a user attempts to undo an action.
  343. *
  344. * @see Syntree.History.undo
  345. * @see Syntree.Action
  346. */
  347. _eventUndo: function() {
  348. Syntree.History.undo();
  349. },
  350. /**
  351. * Code to run when a Node is clicked.
  352. *
  353. * @see Syntree.Node
  354. */
  355. _eventNodeClick: function(e) {
  356. // clickedNode = Syntree.Lib.checkArg(clickedNode, 'svgtextelement');
  357. var node = Syntree.Workspace.page.allElements[$(e.currentTarget).attr('id').split('-')[1]];
  358. if (e.ctrlKey) {
  359. var a = this.page.createMovementArrow(node);
  360. if (Syntree.Lib.checkType(a, 'arrow')) {
  361. Syntree.Workspace.page.select(a);
  362. return false;
  363. }
  364. }
  365. Syntree.Workspace.page.select(node);
  366. },
  367. /**
  368. * Code to run when the user presses the left arrow key.
  369. *
  370. * @see Syntree.Page#navigateHorizontal
  371. */
  372. _eventLeft: function(e) {
  373. if (e.shiftKey && e.ctrlKey) {
  374. this.page.navigateHorizontal('left', true);
  375. } else {
  376. if ($(document.activeElement).hasClass('editor') && $(document.activeElement).val() !== '') {
  377. return;
  378. }
  379. this.page.navigateHorizontal('left');
  380. }
  381. },
  382. /**
  383. * Code to run when the user presses the right arrow key.
  384. *
  385. * @see Syntree.Page#navigateHorizontal
  386. */
  387. _eventRight: function(e) {
  388. if (e.shiftKey && e.ctrlKey) {
  389. this.page.navigateHorizontal('right', true);
  390. } else {
  391. if ($(document.activeElement).hasClass('editor') && $(document.activeElement).val() !== '') {
  392. return;
  393. }
  394. this.page.navigateHorizontal('right');
  395. }
  396. },
  397. /**
  398. * Code to run when the user presses the up arrow key.
  399. *
  400. * @see Syntree.Page#navigateUp
  401. */
  402. _eventUp: function() {
  403. this.page.navigateUp();
  404. },
  405. /**
  406. * Code to run when the user presses the down arrow key.
  407. *
  408. * @see Syntree.Page#navigateDown
  409. */
  410. _eventDown: function(e) {
  411. this.page.navigateDown();
  412. },
  413. /**
  414. * Code to run when the user tries to delete an [Element]{@link Syntree.Element}.
  415. */
  416. _eventDel: function() {
  417. var selected = Syntree.Workspace.page.getSelected();
  418. if (Syntree.Lib.checkType(selected, 'node')) {
  419. if (Syntree.Workspace.page.tree.root === selected) {
  420. var children = Syntree.Workspace.page.tree.root.getChildren().slice();
  421. var c = 0;
  422. while (c < children.length) {
  423. var tree = new Syntree.Tree({
  424. root: children[c],
  425. })
  426. Syntree.Workspace.page.deleteTree(tree);
  427. c++;
  428. }
  429. } else {
  430. var tree = new Syntree.Tree({
  431. root: selected,
  432. })
  433. Syntree.Workspace.page.deleteTree(tree);
  434. }
  435. Syntree.Workspace.page.deselect();
  436. if (!Syntree.Lib.checkType(Syntree.Workspace.page.getSelected(), 'node')) {
  437. Syntree.Workspace.page.select(Syntree.Workspace.page.tree.getRoot());
  438. }
  439. } else {
  440. selected.delete();
  441. }
  442. },
  443. /**
  444. * Code to run when the user presses the ESC key.
  445. */
  446. _eventEsc: function() {
  447. this.page.nodeEditing('cancel');
  448. },
  449. /**
  450. * Code to run when the user types in a [Node]{@link Syntree.Node} editor.
  451. */
  452. _eventEditorTyping: function() {
  453. this.page.nodeEditing('update');
  454. },
  455. /**
  456. * Code to run when the user clicks a file type option in the export modal.
  457. * Updates the displayed file suffix.
  458. */
  459. _eventFiletypeLabelClick: function(e) {
  460. var clicked = $(e.currentTarget).children('input');
  461. if ($(clicked).val() == 'bracket-file') {
  462. $('.modal_option__fname span').text('.txt');
  463. } else if ($(clicked).val() == 'tree-file') {
  464. $('.modal_option__fname span').text('.tree');
  465. } else if ($(clicked).val() == 'png') {
  466. $('.modal_option__fname span').text('.png');
  467. }
  468. },
  469. /**
  470. * Code for exporting the current tree as an image (png).
  471. */
  472. _eventExportImage: function() {
  473. // Get the tree's bounding path, and other initial values.
  474. var path = Syntree.Workspace.page.tree._getPath();
  475. var width = path.rightBound - path.leftBound;
  476. var height = path.bottomBound - path.topBound;
  477. var offsetX = (-1 * path.leftBound + 25);
  478. var offsetY = (-1 * path.topBound + 25);
  479. // Get the SVG data.
  480. var svgstring = '<svg>' + this.page.getSVGString() + '</svg>';
  481. // Set up the canvas size.
  482. $('#export-image-canvas').attr('width', (width + 50));
  483. $('#export-image-canvas').attr('height', (height + 50));
  484. // Use canvg to paint the SVG data to the canvas.
  485. canvg('export-image-canvas', svgstring, {
  486. offsetX: (-1 * (path.leftBound - 25)),
  487. offsetY: (-1 * (path.topBound - 25)),
  488. });
  489. // Download the image.
  490. var canvas = document.getElementById('export-image-canvas');
  491. var imgd = canvas.toDataURL('image/png');
  492. var link = '<a id="temp-file-download" href="' + imgd + '" download="mytree.png"></a>';
  493. $('body').append(link);
  494. $(link)[0].click();
  495. },
  496. /**
  497. * Code for exporting the current tree as a tree file.
  498. */
  499. _eventExportTreeFile: function() {
  500. var fname = $('.modal_option__fname input').val();
  501. var treestring = this.page.tree.getTreestring();
  502. if (Syntree.Lib.checkType(this.export_tree_script, 'string')) {
  503. $.post(this.export_tree_script, {fname: fname, type: 'tree-file', treestring: treestring}, function(link){
  504. $('body').append(link);
  505. $('#temp-file-download')[0].click();
  506. $('#temp-file-download').remove();
  507. })
  508. }
  509. },
  510. /**
  511. * Code for exporting the current tree as bracket notation (.txt file).
  512. */
  513. _eventExportBrackets: function() {
  514. $('.loading-icon').show();
  515. // Get fname
  516. var fname = $('.modal_option__fname input').val();
  517. // Get brackets
  518. var brackets = this.page.tree.getBracketNotation();
  519. // Post it
  520. if (Syntree.Lib.checkType(this.export_tree_script, 'string')) {
  521. $.post(this.export_tree_script, {fname: fname, type: 'bracket-file', brackets: brackets}, function(link) {
  522. $('body').append(link);
  523. $('#temp-file-download')[0].click();
  524. $('#temp-file-download').remove();
  525. $('.loading-icon').hide();
  526. });
  527. }
  528. },
  529. /**
  530. * Code to run when the user presses Enter.
  531. */
  532. _eventEnter: function() {
  533. this.page.nodeEditing('toggle');
  534. },
  535. toString: function() {
  536. return '[object Workspace]';
  537. },
  538. _eventFocus: function() {
  539. $('.focus_check_overlay').hide();
  540. $('.focus_check_underlay').show();
  541. window.scrollTo($('#workspace').offset().left,$('#workspace').offset().top);
  542. // $('body').css('overflow','hidden');
  543. $('#workspace_container').css('z-index',103);
  544. this.focused = true;
  545. },
  546. _eventUnfocus: function() {
  547. $('.focus_check_overlay').show();
  548. $('.focus_check_underlay').hide();
  549. $('body').css('overflow','initial');
  550. $('#workspace_container').css('z-index',0);
  551. this.focused = false;
  552. },
  553. // IMPORTANT:
  554. // All this stuff is on hold. Please ignore it.
  555. _eventBGClick: function(e) {
  556. return; //temporary
  557. var x = e.pageX - $('#workspace').offset().left;
  558. var y = e.pageY - $('#workspace').offset().top;
  559. var nearest = this.page.getNearestNode(x,y);
  560. var newNode = new Syntree.Node(0,0);
  561. if (Syntree.Lib.checkType(nearest, 'object')) {
  562. if (nearest.deltaY < -10) {
  563. if (nearest.deltaX > 0) {
  564. nearest.node.addChild(newNode,0);
  565. } else {
  566. nearest.node.addChild(newNode);
  567. }
  568. } else {
  569. var childIndex = nearest.node.getParent().getChildren().indexOf(nearest.node);
  570. if (nearest.deltaX > 0) {
  571. nearest.node.addChild(newNode,childIndex);
  572. } else {
  573. nearest.node.addChild(newNode,childIndex+1);
  574. }
  575. }
  576. }
  577. },
  578. _eventUpload: function() {
  579. var W = this;
  580. $('body').append('<input type="file" id="temp-choose-file">');
  581. $('#temp-choose-file').change(function() {
  582. var f = document.getElementById('temp-choose-file').files[0];
  583. if (f) {
  584. var reader = new FileReader();
  585. reader.readAsText(f, 'UTF-8');
  586. reader.onload = function (e) {
  587. W.page.openTree(e.target.result);
  588. }
  589. reader.onerror = function (e) {
  590. alert('Unable to read file. Please upload a .tree file.')
  591. }
  592. }
  593. $('#temp-choose-file').remove();
  594. });
  595. $('#temp-choose-file').click();
  596. },
  597. _eventSave: function() {
  598. var treestring = this.page.tree.getTreestring();
  599. var W = this;
  600. if (Syntree.Lib.checkType(this.save_tree_script, 'string')) {
  601. $.post(this.save_tree_script,{treestring:treestring,treeid:this.page.tree.getId()},function(result){
  602. if (Number(result)) {
  603. if (!Syntree.Lib.checkType(Syntree.Workspace.page.tree.getId(), 'number')) {
  604. W.page.tree.setId(Number(result));
  605. }
  606. alert('Saved');
  607. } else {
  608. alert('Sorry, there was a problem saving your tree');
  609. }
  610. });
  611. }
  612. },
  613. }