1. 1 : import { getLibrary, getNetworkFromLibrary } from './networkLibrary';
  2. 2 :
  3. 3 : import { Gate } from '../editorElements';
  4. 4 :
  5. 5 : /**
  6. 6 : * Item in the [ContextMenu](./module-ContextMenu.html). ContextMenuItems can be nested using the appendItem function.
  7. 7 : */
  8. 8 : class ContextMenuItem {
  9. 9 : /**
  10. 10 : * @param {string} text text on the button
  11. 11 : * @param {ContextMenu} contextMenu instance of the [ContextMenu](./module-ContextMenu.html) this item belongs to
  12. 12 : * @param {Function} clickFunction callback function that will be called when user clicks this item
  13. 13 : */
  14. 14 : constructor(text, contextMenu, clickFunction) {
  15. 15 : /**
  16. 16 : * text on the button
  17. 17 : * @type {string}
  18. 18 : */
  19. 19 : this.text = text;
  20. 20 :
  21. 21 : /**
  22. 22 : * instance of the [ContextMenu](./module-ContextMenu.html) this item belongs to
  23. 23 : * @type {ContextMenu}
  24. 24 : */
  25. 25 : this.contextMenu = contextMenu;
  26. 26 :
  27. 27 : /**
  28. 28 : * jQuery element representing DOM content of this menu item
  29. 29 : * @type {jQuery.element}
  30. 30 : */
  31. 31 : this.$el = $('<li>').text(text);
  32. 32 :
  33. 33 : // set up click callback if clickFunction is defined
  34. 34 : if (clickFunction !== undefined) {
  35. 35 : $(this.$el).click(event => {
  36. 36 : clickFunction();
  37. 37 : contextMenu.hide();
  38. 38 :
  39. 39 : event.stopPropagation();
  40. 40 : });
  41. 41 : }
  42. 42 :
  43. 43 : /**
  44. 44 : * jQuery element containing the submenu (or undefined, if item has no subitems)
  45. 45 : * @type {jQuery.element}
  46. 46 : */
  47. 47 : this.$submenu = undefined;
  48. 48 :
  49. 49 : /**
  50. 50 : * submenu item counter
  51. 51 : * @type {Number}
  52. 52 : */
  53. 53 : this.itemCount = 0;
  54. 54 :
  55. 55 : // set hover callback
  56. 56 : $(this.$el).hover(
  57. 57 : event => {
  58. 58 : // mouse on
  59. 59 :
  60. 60 : if (this.length > 0) {
  61. 61 : this.$submenu.css({
  62. 62 : display: 'block',
  63. 63 : top: this.$el.offset().top,
  64. 64 : left: this.$el.parent().offset().left + this.$el.parent().width()
  65. 65 : });
  66. 66 :
  67. 67 : this.contextMenu.$el.after(this.$submenu);
  68. 68 :
  69. 69 : event.stopPropagation();
  70. 70 : }
  71. 71 : },
  72. 72 : () => {
  73. 73 : // mouse out
  74. 74 : if (this.$submenu) {
  75. 75 : this.$submenu.css({
  76. 76 : display: 'none'
  77. 77 : });
  78. 78 : }
  79. 79 :
  80. 80 : // do not stop event propagation, here it is wanted
  81. 81 : // (because submenu overrides display: none when user moves from this menu item to the submenu)
  82. 82 : }
  83. 83 : );
  84. 84 : }
  85. 85 :
  86. 86 : /**
  87. 87 : * instance of [App](./module-App.html) this menu belongs to
  88. 88 : * @type {App}
  89. 89 : */
  90. 90 : get appInstance() {
  91. 91 : return this.contextMenu.appInstance;
  92. 92 : }
  93. 93 :
  94. 94 : /**
  95. 95 : * number of items in the submenu
  96. 96 : * @return {Number}
  97. 97 : */
  98. 98 : get length() {
  99. 99 : return this.itemCount;
  100. 100 : }
  101. 101 :
  102. 102 : /**
  103. 103 : * add a CSS class to this item
  104. 104 : * @param {string} cls [description]
  105. 105 : */
  106. 106 : addClass(cls) {
  107. 107 : this.$el.addClass(cls);
  108. 108 : return this;
  109. 109 : }
  110. 110 :
  111. 111 : /**
  112. 112 : * append a nested {@link ContextMenuItem} to this item
  113. 113 : * @param {ContextMenuItem} item item that will be appended
  114. 114 : */
  115. 115 : appendItem(item) {
  116. 116 : if (!this.$submenu) {
  117. 117 : this.$submenu = $('<ul>').addClass('subList');
  118. 118 : this.$submenu.hover(
  119. 119 : () => {
  120. 120 : this.$submenu.css('display', 'block');
  121. 121 : },
  122. 122 : () => {
  123. 123 : this.$submenu.css('display', 'none');
  124. 124 : }
  125. 125 : );
  126. 126 : }
  127. 127 : this.$submenu.append(item.$el);
  128. 128 :
  129. 129 : this.itemCount++;
  130. 130 :
  131. 131 : return item;
  132. 132 : }
  133. 133 :
  134. 134 : /**
  135. 135 : * get jQuery element of this menu item
  136. 136 : * @return {jQuery.element} jQuery element containing all DOM content for this menu item
  137. 137 : */
  138. 138 : get jQuery() {
  139. 139 : return this.$el;
  140. 140 : }
  141. 141 :
  142. 142 : get jQuerySubmenu() {
  143. 143 : return this.$submenu;
  144. 144 : }
  145. 145 : }
  146. 146 :
  147. 147 : /**
  148. 148 : * Menu item that has a custom click callback function that adds a {@link Gate} of the specified type to the [App](./module-App.html)
  149. 149 : * @extends ContextMenuItem
  150. 150 : */
  151. 151 : class GateMenuItem extends ContextMenuItem {
  152. 152 : /**
  153. 153 : * @param {string} type type of the gate {@link Gate} (and, or, ...)
  154. 154 : * @param {ContextMenu} contextMenu instance of the [ContextMenu](./module-ContextMenu.html) that this item belongs to
  155. 155 : */
  156. 156 : constructor(type, contextMenu) {
  157. 157 : super(`${type.toUpperCase()} gate`, contextMenu, () => {
  158. 158 : this.appInstance.newGate(
  159. 159 : type,
  160. 160 : this.appInstance.snapToGrid(
  161. 161 : this.appInstance.viewbox.transformX(contextMenu.position.x)
  162. 162 : ),
  163. 163 : this.appInstance.snapToGrid(
  164. 164 : this.appInstance.viewbox.transformY(contextMenu.position.y)
  165. 165 : )
  166. 166 : );
  167. 167 : });
  168. 168 : }
  169. 169 : }
  170. 170 :
  171. 171 : /**
  172. 172 : * Menu item that has a custom click callback function that adds a specified {@link Blackbox} to the [App](./module-App.html)
  173. 173 : * @extends ContextMenuItem
  174. 174 : */
  175. 175 : class BlackboxMenuItem extends ContextMenuItem {
  176. 176 : constructor(name, file, contextMenu) {
  177. 177 : super(name, contextMenu, () => {
  178. 178 : getNetworkFromLibrary(file)
  179. 179 : .then(({ blackbox, name }) => {
  180. 180 : const { inputs, outputs, table } = blackbox;
  181. 181 :
  182. 182 : // use the name specified in the blackbox item, if it does not exist, use the name for the network
  183. 183 : let usedName = blackbox.name || name;
  184. 184 :
  185. 185 : this.appInstance.newBlackbox(
  186. 186 : inputs,
  187. 187 : outputs,
  188. 188 : table,
  189. 189 : usedName,
  190. 190 : this.appInstance.snapToGrid(
  191. 191 : this.appInstance.viewbox.transformX(contextMenu.position.x)
  192. 192 : ),
  193. 193 : this.appInstance.snapToGrid(
  194. 194 : this.appInstance.viewbox.transformY(contextMenu.position.y)
  195. 195 : )
  196. 196 : );
  197. 197 : })
  198. 198 : .catch(error => {
  199. 199 : console.error(error);
  200. 200 : });
  201. 201 : });
  202. 202 : }
  203. 203 : }
  204. 204 :
  205. 205 : /**
  206. 206 : * Menu item that has a custom click callback function that adds a specified Network to the [App](./module-App.html)
  207. 207 : * @extends ContextMenuItem
  208. 208 : */
  209. 209 : class NetworkMenuItem extends ContextMenuItem {
  210. 210 : constructor(name, file, contextMenu) {
  211. 211 : super(name, contextMenu, () => {
  212. 212 : getNetworkFromLibrary(file)
  213. 213 : .then(data => {
  214. 214 : this.appInstance
  215. 215 : .importData(
  216. 216 : data,
  217. 217 : Math.round(
  218. 218 : this.appInstance.viewbox.transformX(contextMenu.position.x) /
  219. 219 : this.appInstance.gridSize
  220. 220 : ),
  221. 221 : Math.round(
  222. 222 : this.appInstance.viewbox.transformY(contextMenu.position.y) /
  223. 223 : this.appInstance.gridSize
  224. 224 : )
  225. 225 : )
  226. 226 : .then(warnings => {
  227. 227 : for (const warning of warnings) {
  228. 228 : this.appInstance.messages.newWarningMessage(warning);
  229. 229 : }
  230. 230 : });
  231. 231 : })
  232. 232 : .catch(error => {
  233. 233 : this.appInstance.messages.newErrorMessage(error);
  234. 234 : });
  235. 235 : });
  236. 236 : }
  237. 237 : }
  238. 238 :
  239. 239 : /** @module ContextMenu */
  240. 240 : /**
  241. 241 : * ContextMenu represents the menu that is displayed to the user when they right click on a canvas.
  242. 242 : * This menu allows user to add elements to the canvas and in the case that user rightclicked
  243. 243 : * on a specific element, this menu allows them to remove this element.
  244. 244 : */
  245. 245 : export default class ContextMenu {
  246. 246 : /**
  247. 247 : * @param {App} appInstance instance of [App](./module-App.html) this menu belongs to
  248. 248 : */
  249. 249 : constructor(appInstance) {
  250. 250 : /**
  251. 251 : * instance of [App](./module-App.html) this menu belongs to
  252. 252 : * @type {App}
  253. 253 : */
  254. 254 : this.appInstance = appInstance;
  255. 255 :
  256. 256 : /**
  257. 257 : * Position of the context menu. It is used to add the new elements to the correct position on the canvas.
  258. 258 : * @type {Object}
  259. 259 : */
  260. 260 : this.position = {
  261. 261 : x: 0,
  262. 262 : y: 0
  263. 263 : };
  264. 264 :
  265. 265 : /**
  266. 266 : * jQuery element containing the context menu
  267. 267 : * @type {jQuery.element}
  268. 268 : */
  269. 269 : this.$el = $('<ul>');
  270. 270 : this.$el.attr('id', 'contextMenu');
  271. 271 :
  272. 272 : let special = new ContextMenuItem('Special elements', this);
  273. 273 :
  274. 274 : // add input box
  275. 275 : special.appendItem(
  276. 276 : new ContextMenuItem('Input box', this, () => {
  277. 277 : let position = {
  278. 278 : left: this.appInstance.snapToGrid(
  279. 279 : appInstance.viewbox.transformX(this.position.x)
  280. 280 : ),
  281. 281 : top: this.appInstance.snapToGrid(
  282. 282 : appInstance.viewbox.transformY(this.position.y)
  283. 283 : )
  284. 284 : };
  285. 285 :
  286. 286 : appInstance.newInput(position.left, position.top);
  287. 287 : })
  288. 288 : );
  289. 289 :
  290. 290 : // add output box
  291. 291 : special.appendItem(
  292. 292 : new ContextMenuItem('Output box', this, () => {
  293. 293 : let position = {
  294. 294 : left: this.appInstance.snapToGrid(
  295. 295 : appInstance.viewbox.transformX(this.position.x)
  296. 296 : ),
  297. 297 : top: this.appInstance.snapToGrid(
  298. 298 : appInstance.viewbox.transformY(this.position.y)
  299. 299 : )
  300. 300 : };
  301. 301 :
  302. 302 : appInstance.newOutput(position.left, position.top);
  303. 303 : })
  304. 304 : );
  305. 305 :
  306. 306 : this.appendItem(special);
  307. 307 :
  308. 308 : // list of gates that can be added
  309. 309 : const gates = Gate.validGates;
  310. 310 : let gateList = new ContextMenuItem('New gate', this, appInstance);
  311. 311 : for (const name of gates) {
  312. 312 : gateList.appendItem(new GateMenuItem(name, this));
  313. 313 : }
  314. 314 : this.appendItem(gateList);
  315. 315 :
  316. 316 : // more options will be added in the getLibrary() callback below
  317. 317 : let networkList = new ContextMenuItem('Add a network', this);
  318. 318 : networkList.appendItem(
  319. 319 : new ContextMenuItem('Paste a network', this, () => {
  320. 320 : this.displayImportDialog();
  321. 321 : })
  322. 322 : );
  323. 323 : this.appendItem(networkList); // always append
  324. 324 :
  325. 325 : let blackboxList = new ContextMenuItem('Add a blackbox', this); // appends only if contains items (see the callback)
  326. 326 :
  327. 327 : // network import (blackbox, network)
  328. 328 : getLibrary()
  329. 329 : .then(networks => {
  330. 330 : for (const { name, file, hasTable, hasNetwork } of networks) {
  331. 331 : // add a network as a blackbox
  332. 332 : if (hasTable) {
  333. 333 : blackboxList.appendItem(new BlackboxMenuItem(name, file, this));
  334. 334 : }
  335. 335 :
  336. 336 : // load a network as a network of components connected with wires
  337. 337 : if (hasNetwork) {
  338. 338 : networkList.appendItem(new NetworkMenuItem(name, file, this));
  339. 339 : }
  340. 340 : }
  341. 341 :
  342. 342 : if (blackboxList.length > 0) {
  343. 343 : this.appendItem(blackboxList);
  344. 344 : }
  345. 345 : })
  346. 346 : .catch(error => {
  347. 347 : console.error(error);
  348. 348 : });
  349. 349 :
  350. 350 : // add conditional items for box and wire removal
  351. 351 : this.appendConditionalItem('box', 'Remove this item', id => {
  352. 352 : this.appInstance.removeBox(id);
  353. 353 : });
  354. 354 : this.appendConditionalItem('wire', 'Remove this wire', id => {
  355. 355 : this.appInstance.removeWireById(id);
  356. 356 : });
  357. 357 :
  358. 358 : // add the context menu to the DOM
  359. 359 : appInstance.$svg.before(this.$el);
  360. 360 :
  361. 361 : /**
  362. 362 : * Number of items in this menu (used in the .lenght getter). Conditional items do not count.
  363. 363 : * @type {Number}
  364. 364 : */
  365. 365 : this.itemCount = 0;
  366. 366 : }
  367. 367 :
  368. 368 : get length() {
  369. 369 : return this.itemCount;
  370. 370 : }
  371. 371 :
  372. 372 : /**
  373. 373 : * append a context menu item to the context menu
  374. 374 : * @param {ContextMenuItem} item instance of {@link ContextMenuItem} that will be added to this menu
  375. 375 : */
  376. 376 : appendItem(item) {
  377. 377 : this.$el.append(item.jQuery);
  378. 378 :
  379. 379 : this.itemCount++;
  380. 380 :
  381. 381 : return item;
  382. 382 : }
  383. 383 :
  384. 384 : /**
  385. 385 : * appends an connditional item (that is shown only if the target has the class itemClass)
  386. 386 : * @param {string} itemClass show the item only if the target has this class
  387. 387 : * @param {string} text text of this menu item
  388. 388 : * @param {Function} clickFunction function with one argument (ID of the target) that will be called on click
  389. 389 : */
  390. 390 : appendConditionalItem(itemClass, text, clickFunction) {
  391. 391 : if (!this.conditionalItems) {
  392. 392 : this.conditionalItems = [];
  393. 393 : }
  394. 394 :
  395. 395 : this.conditionalItems[this.conditionalItems.length] = {
  396. 396 : itemClass: itemClass,
  397. 397 : text: text,
  398. 398 : clickFunction: clickFunction
  399. 399 : };
  400. 400 : }
  401. 401 :
  402. 402 : /**
  403. 403 : * display the dialog for importing a network from a clipboard
  404. 404 : */
  405. 405 : displayImportDialog() {
  406. 406 : let $popup = $('<div>')
  407. 407 : .addClass('importExport')
  408. 408 : .addClass('import');
  409. 409 :
  410. 410 : let textareaId = 'importJSON';
  411. 411 : let $textblock = $('<textarea>').attr('id', textareaId);
  412. 412 :
  413. 413 : let lityInstance;
  414. 414 :
  415. 415 : $popup.append($textblock).append(
  416. 416 : $('<a>')
  417. 417 : .attr({
  418. 418 : href: '#',
  419. 419 : class: 'upload'
  420. 420 : })
  421. 421 : .append($('<img>').attr('src', 'img/gui/import.svg'))
  422. 422 : .append(' import from JSON')
  423. 423 : .on('click', () => {
  424. 424 : let data;
  425. 425 :
  426. 426 : try {
  427. 427 : data = JSON.parse($('#' + textareaId).val());
  428. 428 : } catch (e) {
  429. 429 : this.appInstance.messages.newErrorMessage(
  430. 430 : 'The imported file is not a valid JSON file.'
  431. 431 : );
  432. 432 : lityInstance.close();
  433. 433 : }
  434. 434 :
  435. 435 : if (data) {
  436. 436 : // proccess the imported data
  437. 437 : this.appInstance
  438. 438 : .importData(
  439. 439 : data,
  440. 440 : Math.round(
  441. 441 : this.appInstance.viewbox.transformX(this.position.x) /
  442. 442 : this.appInstance.gridSize
  443. 443 : ),
  444. 444 : Math.round(
  445. 445 : this.appInstance.viewbox.transformY(this.position.y) /
  446. 446 : this.appInstance.gridSize
  447. 447 : )
  448. 448 : )
  449. 449 : .then(warnings => {
  450. 450 : for (const warning of warnings) {
  451. 451 : this.appInstance.messages.newWarningMessage(warning);
  452. 452 : }
  453. 453 : })
  454. 454 : .finally(() => {
  455. 455 : lityInstance.close();
  456. 456 : });
  457. 457 : }
  458. 458 : })
  459. 459 : );
  460. 460 :
  461. 461 : lityInstance = lity($popup);
  462. 462 :
  463. 463 : // focus on the textblock
  464. 464 : $textblock.focus();
  465. 465 : }
  466. 466 :
  467. 467 : /**
  468. 468 : * decide whether or not to display specific conditional items
  469. 469 : * @param {jQuery.element} $target jQuery target of a MouseEvent (element that user clicked on)
  470. 470 : */
  471. 471 : resolveConditionalItems($target) {
  472. 472 : for (let item of this.conditionalItems) {
  473. 473 : if ($target.hasClass(item.itemClass)) {
  474. 474 : this.appendItem(
  475. 475 : new ContextMenuItem(item.text, this, () => {
  476. 476 : item.clickFunction($target.attr('id'));
  477. 477 : })
  478. 478 : ).addClass('conditional');
  479. 479 : }
  480. 480 : }
  481. 481 : }
  482. 482 :
  483. 483 : /**
  484. 484 : * hide all conditional items
  485. 485 : */
  486. 486 : hideAllConditionalItems() {
  487. 487 : this.$el.children('.conditional').remove();
  488. 488 : }
  489. 489 :
  490. 490 : /**
  491. 491 : * displays the context menu with the right set of conditional items
  492. 492 : * @param {number} x horizontal position of the context menu in CSS pixels
  493. 493 : * @param {number} y vertical position of the context menu in CSS pixels
  494. 494 : * @param {jQuery.element} $target jQuery target of a MouseEvent (element that user clicked on)
  495. 495 : */
  496. 496 : display(x, y, $target) {
  497. 497 : this.position = {
  498. 498 : x: x,
  499. 499 : y: y
  500. 500 : };
  501. 501 :
  502. 502 : this.resolveConditionalItems($target);
  503. 503 :
  504. 504 : this.$el
  505. 505 : .css({
  506. 506 : display: 'block',
  507. 507 : top: y,
  508. 508 : left: x
  509. 509 : })
  510. 510 : // set the width expicitly, or else the menu will widen when displaying a submenu
  511. 511 : // 2 is to prevent a weird text wrap bug
  512. 512 : .css('width', 'auto')
  513. 513 : .css('width', this.$el.innerWidth() + 2);
  514. 514 : }
  515. 515 :
  516. 516 : /**
  517. 517 : * hide the context menu
  518. 518 : */
  519. 519 : hide() {
  520. 520 : this.$el.css({ display: 'none' });
  521. 521 : $('.subList').css({ display: 'none' });
  522. 522 : this.hideAllConditionalItems();
  523. 523 : }
  524. 524 : }