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