dialogger.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. var fs = require('fs');
  2. // Constants
  3. var con =
  4. {
  5. allowable_connections:
  6. [
  7. ['dialogue.Text', 'dialogue.Text'],
  8. ['dialogue.Text', 'dialogue.Node'],
  9. ['dialogue.Text', 'dialogue.Choice'],
  10. ['dialogue.Text', 'dialogue.Set'],
  11. ['dialogue.Text', 'dialogue.Branch'],
  12. ['dialogue.Text', 'dialogue.Function'],
  13. ['dialogue.Text', 'dialogue.Tree'],
  14. ['dialogue.Text', 'dialogue.GoToLabel'],
  15. ['dialogue.Node', 'dialogue.Text'],
  16. ['dialogue.Node', 'dialogue.Node'],
  17. ['dialogue.Node', 'dialogue.Choice'],
  18. ['dialogue.Node', 'dialogue.Set'],
  19. ['dialogue.Node', 'dialogue.Branch'],
  20. ['dialogue.Node', 'dialogue.Function'],
  21. ['dialogue.Node', 'dialogue.Tree'],
  22. ['dialogue.Node', 'dialogue.GoToLabel'],
  23. ['dialogue.Tree', 'dialogue.Node'],
  24. ['dialogue.Tree', 'dialogue.Text'],
  25. ['dialogue.Tree', 'dialogue.Choice'],
  26. ['dialogue.Tree', 'dialogue.Set'],
  27. ['dialogue.Tree', 'dialogue.Branch'],
  28. ['dialogue.Tree', 'dialogue.Function'],
  29. ['dialogue.Tree', 'dialogue.Tree'],
  30. ['dialogue.Tree', 'dialogue.GoToLabel'],
  31. ['dialogue.StartNode', 'dialogue.Text'],
  32. ['dialogue.StartNode', 'dialogue.Node'],
  33. ['dialogue.StartNode', 'dialogue.Choice'],
  34. ['dialogue.StartNode', 'dialogue.Set'],
  35. ['dialogue.StartNode', 'dialogue.Branch'],
  36. ['dialogue.StartNode', 'dialogue.Function'],
  37. ['dialogue.StartNode', 'dialogue.Tree'],
  38. ['dialogue.StartNode', 'dialogue.GoToLabel'],
  39. ['dialogue.Choice', 'dialogue.Text'],
  40. ['dialogue.Choice', 'dialogue.Node'],
  41. ['dialogue.Choice', 'dialogue.Set'],
  42. ['dialogue.Choice', 'dialogue.Branch'],
  43. ['dialogue.Choice', 'dialogue.Function'],
  44. ['dialogue.Choice', 'dialogue.Tree'],
  45. ['dialogue.Choice', 'dialogue.GoToLabel'],
  46. ['dialogue.Set', 'dialogue.Text'],
  47. ['dialogue.Set', 'dialogue.Node'],
  48. ['dialogue.Set', 'dialogue.Set'],
  49. ['dialogue.Set', 'dialogue.Branch'],
  50. ['dialogue.Set', 'dialogue.Function'],
  51. ['dialogue.Set', 'dialogue.Tree'],
  52. ['dialogue.Set', 'dialogue.GoToLabel'],
  53. ['dialogue.Function', 'dialogue.Text'],
  54. ['dialogue.Function', 'dialogue.Node'],
  55. ['dialogue.Function', 'dialogue.Set'],
  56. ['dialogue.Function', 'dialogue.Branch'],
  57. ['dialogue.Function', 'dialogue.Function'],
  58. ['dialogue.Function', 'dialogue.Tree'],
  59. ['dialogue.Function', 'dialogue.GoToLabel'],
  60. ['dialogue.Branch', 'dialogue.Text'],
  61. ['dialogue.Branch', 'dialogue.Node'],
  62. ['dialogue.Branch', 'dialogue.Set'],
  63. ['dialogue.Branch', 'dialogue.Branch'],
  64. ['dialogue.Branch', 'dialogue.Function'],
  65. ['dialogue.Branch', 'dialogue.Tree'],
  66. ['dialogue.Branch', 'dialogue.GoToLabel'],
  67. ],
  68. default_link: new joint.dia.Link(
  69. {
  70. attrs:
  71. {
  72. '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z', },
  73. '.link-tools .tool-remove circle, .marker-vertex': { r: 8 },
  74. },
  75. }),
  76. };
  77. con.default_link.set('smooth', true);
  78. // State
  79. var state =
  80. {
  81. graph: new joint.dia.Graph(),
  82. paper: null,
  83. filepath: null,
  84. panning: false,
  85. mouse_position: { x: 0, y: 0 },
  86. context_position: { x: 0, y: 0 },
  87. menu: null,
  88. };
  89. // Models
  90. joint.shapes.dialogue = {};
  91. joint.shapes.dialogue.Base = joint.shapes.devs.Model.extend(
  92. {
  93. defaults: joint.util.deepSupplement
  94. (
  95. {
  96. type: 'dialogue.Base',
  97. size: { width: 100, height: 64 },
  98. name: '',
  99. attrs:
  100. {
  101. rect: { stroke: 'none', 'fill-opacity': 0 },
  102. text: { display: 'none' },
  103. },
  104. },
  105. joint.shapes.devs.Model.prototype.defaults
  106. ),
  107. });
  108. joint.shapes.dialogue.BaseView = joint.shapes.devs.ModelView.extend(
  109. {
  110. template:
  111. [
  112. '<div class="node">',
  113. '<span class="label"></span>',
  114. '<button class="delete">x</button>',
  115. '<textarea class="name" placeholder="Text"></textarea>',
  116. '</div>',
  117. ].join(''),
  118. initialize: function()
  119. {
  120. _.bindAll(this, 'updateBox');
  121. joint.shapes.devs.ModelView.prototype.initialize.apply(this, arguments);
  122. this.$box = $(_.template(this.template)());
  123. // Prevent paper from handling pointerdown.
  124. this.$box.find('textarea, input').on('mousedown click', function(evt) { evt.stopPropagation(); });
  125. // This is an example of reacting on the input change and storing the input data in the cell model.
  126. this.$box.find('textarea.name, input.name').on('change', _.bind(function(evt)
  127. {
  128. this.model.set('name', $(evt.target).val());
  129. }, this));
  130. this.$box.find('.delete').on('click', _.bind(this.model.remove, this.model));
  131. // Update the box position whenever the underlying model changes.
  132. this.model.on('change', this.updateBox, this);
  133. // Remove the box when the model gets removed from the graph.
  134. this.model.on('remove', this.removeBox, this);
  135. this.updateBox();
  136. },
  137. render: function()
  138. {
  139. joint.shapes.devs.ModelView.prototype.render.apply(this, arguments);
  140. this.paper.$el.prepend(this.$box);
  141. this.updateBox();
  142. return this;
  143. },
  144. updateBox: function()
  145. {
  146. // Set the position and dimension of the box so that it covers the JointJS element.
  147. var bbox = this.model.getBBox();
  148. // Example of updating the HTML with a data stored in the cell model.
  149. var nameField = this.$box.find('textarea.name, input.name');
  150. if (!nameField.is(':focus'))
  151. nameField.val(this.model.get('name'));
  152. var label = this.$box.find('.label');
  153. var type = this.model.get('type').slice('dialogue.'.length);
  154. label.text(type);
  155. label.attr('class', 'label ' + type);
  156. this.$box[0].classList.add(type);
  157. this.$box.css({ width: bbox.width, left: bbox.x, top: bbox.y, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)' });
  158. },
  159. removeBox: function(evt)
  160. {
  161. this.$box.remove();
  162. },
  163. });
  164. joint.shapes.dialogue.Node = joint.shapes.devs.Model.extend(
  165. {
  166. defaults: joint.util.deepSupplement
  167. (
  168. {
  169. type: 'dialogue.Node',
  170. size: { width: 100, height: 65, },
  171. inPorts: ['input'],
  172. outPorts: ['output'],
  173. attrs:
  174. {
  175. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  176. },
  177. },
  178. joint.shapes.dialogue.Base.prototype.defaults
  179. ),
  180. });
  181. joint.shapes.dialogue.NodeView = joint.shapes.dialogue.BaseView;
  182. joint.shapes.dialogue.Tree = joint.shapes.devs.Model.extend(
  183. {
  184. defaults: joint.util.deepSupplement
  185. (
  186. {
  187. type: 'dialogue.Tree',
  188. size: { width: 150, height: 65, },
  189. inPorts: ['input'],
  190. outPorts: ['output'],
  191. attrs:
  192. {
  193. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  194. },
  195. },
  196. joint.shapes.dialogue.Base.prototype.defaults
  197. ),
  198. });
  199. joint.shapes.dialogue.TreeView = joint.shapes.dialogue.BaseView;
  200. joint.shapes.dialogue.Text = joint.shapes.devs.Model.extend(
  201. {
  202. defaults: joint.util.deepSupplement
  203. (
  204. {
  205. type: 'dialogue.Text',
  206. size: { width: 350 },
  207. inPorts: ['input'],
  208. outPorts: ['output'],
  209. attrs:
  210. {
  211. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  212. },
  213. },
  214. joint.shapes.dialogue.Base.prototype.defaults
  215. ),
  216. });
  217. joint.shapes.dialogue.TextView = joint.shapes.dialogue.BaseView;
  218. joint.shapes.dialogue.Function = joint.shapes.devs.Model.extend(
  219. {
  220. defaults: joint.util.deepSupplement
  221. (
  222. {
  223. type: 'dialogue.Function',
  224. size: { width: 200, height: 65, },
  225. inPorts: ['input'],
  226. outPorts: ['output'],
  227. attrs:
  228. {
  229. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  230. },
  231. },
  232. joint.shapes.dialogue.Base.prototype.defaults
  233. ),
  234. });
  235. joint.shapes.dialogue.FunctionView = joint.shapes.dialogue.BaseView;
  236. joint.shapes.dialogue.GoToLabel = joint.shapes.devs.Model.extend(
  237. {
  238. defaults: joint.util.deepSupplement
  239. (
  240. {
  241. type: 'dialogue.GoToLabel',
  242. size: { width: 125, height: 65, },
  243. inPorts: ['input'],
  244. outPorts: ['output'],
  245. attrs:
  246. {
  247. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  248. },
  249. },
  250. joint.shapes.dialogue.Base.prototype.defaults
  251. ),
  252. });
  253. joint.shapes.dialogue.GoToLabelView = joint.shapes.dialogue.BaseView;
  254. joint.shapes.dialogue.Branch = joint.shapes.devs.Model.extend(
  255. {
  256. defaults: joint.util.deepSupplement
  257. (
  258. {
  259. type: 'dialogue.Branch',
  260. size: { width: 150, height: 95, },
  261. inPorts: ['input'],
  262. outPorts: ['output0'],
  263. values: [],
  264. },
  265. joint.shapes.dialogue.Base.prototype.defaults
  266. ),
  267. });
  268. joint.shapes.dialogue.BranchView = joint.shapes.dialogue.BaseView.extend(
  269. {
  270. template:
  271. [
  272. '<div class="node">',
  273. '<span class="label"></span>',
  274. '<button class="delete">x</button>',
  275. '<button class="add">+</button>',
  276. '<button class="remove">-</button>',
  277. '<input type="text" class="name" placeholder="Variable" />',
  278. '<input type="text" value="Default" readonly/>',
  279. '</div>',
  280. ].join(''),
  281. initialize: function()
  282. {
  283. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  284. this.$box.find('.add').on('click', _.bind(this.addPort, this));
  285. this.$box.find('.remove').on('click', _.bind(this.removePort, this));
  286. },
  287. removePort: function()
  288. {
  289. if (this.model.get('outPorts').length > 1)
  290. {
  291. var outPorts = this.model.get('outPorts').slice(0);
  292. outPorts.pop();
  293. this.model.set('outPorts', outPorts);
  294. var values = this.model.get('values').slice(0);
  295. values.pop();
  296. this.model.set('values', values);
  297. this.updateSize();
  298. }
  299. },
  300. addPort: function()
  301. {
  302. var outPorts = this.model.get('outPorts').slice(0);
  303. outPorts.push('output' + outPorts.length.toString());
  304. this.model.set('outPorts', outPorts);
  305. var values = this.model.get('values').slice(0);
  306. values.push(null);
  307. this.model.set('values', values);
  308. this.updateSize();
  309. },
  310. updateBox: function()
  311. {
  312. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  313. var values = this.model.get('values');
  314. var valueFields = this.$box.find('input.value');
  315. // Add value fields if necessary
  316. for (var i = valueFields.length; i < values.length; i++)
  317. {
  318. // Prevent paper from handling pointerdown.
  319. var $field = $('<input type="text" class="value" />');
  320. $field.attr('placeholder', 'Value ' + (i + 1).toString());
  321. $field.attr('index', i);
  322. this.$box.append($field);
  323. $field.on('mousedown click', function(evt) { evt.stopPropagation(); });
  324. // This is an example of reacting on the input change and storing the input data in the cell model.
  325. $field.on('change', _.bind(function(evt)
  326. {
  327. var values = this.model.get('values').slice(0);
  328. values[$(evt.target).attr('index')] = $(evt.target).val();
  329. this.model.set('values', values);
  330. }, this));
  331. }
  332. // Remove value fields if necessary
  333. for (var i = values.length; i < valueFields.length; i++)
  334. $(valueFields[i]).remove();
  335. // Update value fields
  336. valueFields = this.$box.find('input.value');
  337. for (var i = 0; i < valueFields.length; i++)
  338. {
  339. var field = $(valueFields[i]);
  340. if (!field.is(':focus'))
  341. field.val(values[i]);
  342. }
  343. },
  344. updateSize: function()
  345. {
  346. var textField = this.$box.find('input.name');
  347. var height = textField.outerHeight(true);
  348. this.model.set('size', { width: 150, height: 95 + Math.max(0, (this.model.get('outPorts').length - 1) * height) });
  349. },
  350. });
  351. joint.shapes.dialogue.StartNode = joint.shapes.devs.Model.extend(
  352. {
  353. defaults: joint.util.deepSupplement
  354. (
  355. {
  356. type: 'dialogue.StartNode',
  357. inPorts: ['input'],
  358. outPorts: ['output'],
  359. size: { width: 100, height: 30, },
  360. value: 'DialogueNode.START',
  361. attrs:
  362. {
  363. '.outPorts circle': { unlimitedConnections: ['dialogue.Choice'], }
  364. },
  365. },
  366. joint.shapes.dialogue.Base.prototype.defaults
  367. ),
  368. });
  369. joint.shapes.dialogue.StartNodeView = joint.shapes.dialogue.BaseView.extend(
  370. {
  371. template:
  372. [
  373. '<div class="node">',
  374. '<span class="label"></span>',
  375. '<button class="delete">x</button>',
  376. '</div>',
  377. ].join(''),
  378. initialize: function()
  379. {
  380. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  381. //this.$box.find('input.value').on('change', _.bind(function(evt)
  382. //{
  383. // this.model.set('value', $(evt.target).val());
  384. //}, this));
  385. },
  386. updateBox: function()
  387. {
  388. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  389. //var field = this.$box.find('input.value');
  390. //if (!field.is(':focus'))
  391. // field.val(this.model.get('value'));
  392. },
  393. });
  394. joint.shapes.dialogue.Choice = joint.shapes.devs.Model.extend(
  395. {
  396. defaults: joint.util.deepSupplement
  397. (
  398. {
  399. type: 'dialogue.Choice',
  400. inPorts: ['input'],
  401. outPorts: ['output'],
  402. size: { width: 175, height: 95, },
  403. value: '',
  404. },
  405. joint.shapes.dialogue.Base.prototype.defaults
  406. ),
  407. });
  408. joint.shapes.dialogue.ChoiceView = joint.shapes.dialogue.BaseView.extend(
  409. {
  410. template:
  411. [
  412. '<div class="node">',
  413. '<span class="label"></span>',
  414. '<button class="delete">x</button>',
  415. '<input type="text" class="name" placeholder="Text" />',
  416. '<input type="text" class="value" placeholder="Conditions" />',
  417. '</div>',
  418. ].join(''),
  419. initialize: function()
  420. {
  421. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  422. this.$box.find('input.value').on('change', _.bind(function(evt)
  423. {
  424. this.model.set('value', $(evt.target).val());
  425. }, this));
  426. },
  427. updateBox: function()
  428. {
  429. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  430. var field = this.$box.find('input.value');
  431. if (!field.is(':focus'))
  432. field.val(this.model.get('value'));
  433. },
  434. });
  435. joint.shapes.dialogue.Set = joint.shapes.devs.Model.extend(
  436. {
  437. defaults: joint.util.deepSupplement
  438. (
  439. {
  440. type: 'dialogue.Set',
  441. inPorts: ['input'],
  442. outPorts: ['output'],
  443. size: { width: 125, height: 90, },
  444. value: '',
  445. },
  446. joint.shapes.dialogue.Base.prototype.defaults
  447. ),
  448. });
  449. joint.shapes.dialogue.SetView = joint.shapes.dialogue.BaseView.extend(
  450. {
  451. template:
  452. [
  453. '<div class="node">',
  454. '<span class="label"></span>',
  455. '<button class="delete">x</button>',
  456. '<input type="text" class="name" placeholder="Variable" />',
  457. '<input type="text" class="value" placeholder="Value" />',
  458. '</div>',
  459. ].join(''),
  460. initialize: function()
  461. {
  462. joint.shapes.dialogue.BaseView.prototype.initialize.apply(this, arguments);
  463. this.$box.find('input.value').on('change', _.bind(function(evt)
  464. {
  465. this.model.set('value', $(evt.target).val());
  466. }, this));
  467. },
  468. updateBox: function()
  469. {
  470. joint.shapes.dialogue.BaseView.prototype.updateBox.apply(this, arguments);
  471. var field = this.$box.find('input.value');
  472. if (!field.is(':focus'))
  473. field.val(this.model.get('value'));
  474. },
  475. });
  476. // Functions
  477. var func = {};
  478. func.validate_connection = function(cellViewS, magnetS, cellViewT, magnetT, end, linkView)
  479. {
  480. // Prevent loop linking
  481. if (magnetS === magnetT)
  482. return false;
  483. if (cellViewS === cellViewT)
  484. return false;
  485. if ($(magnetT).parents('.outPorts').length > 0) // Can't connect to an output port
  486. return false;
  487. var sourceType = cellViewS.model.attributes.type;
  488. var targetType = cellViewT.model.attributes.type;
  489. var valid = false;
  490. for (var i = 0; i < con.allowable_connections.length; i++)
  491. {
  492. var rule = con.allowable_connections[i];
  493. if (sourceType === rule[0] && targetType === rule[1])
  494. {
  495. valid = true;
  496. break;
  497. }
  498. }
  499. if (!valid)
  500. return false;
  501. var links = state.graph.getConnectedLinks(cellViewS.model);
  502. for (var i = 0; i < links.length; i++)
  503. {
  504. var link = links[i];
  505. if (link.attributes.source.id === cellViewS.model.id && link.attributes.source.port === magnetS.attributes.port.nodeValue && link.attributes.target.id)
  506. {
  507. var targetCell = state.graph.getCell(link.attributes.target.id);
  508. if (targetCell.attributes.type !== targetType)
  509. return false; // We can only connect to multiple targets of the same type
  510. if (targetCell === cellViewT.model)
  511. return false; // Already connected
  512. }
  513. }
  514. return true;
  515. };
  516. func.validate_magnet = function(cellView, magnet)
  517. {
  518. if ($(magnet).parents('.outPorts').length === 0)
  519. return false; // we only want output ports
  520. // If unlimited connections attribute is null, we can only ever connect to one object
  521. // If it is not null, it is an array of type strings which are allowed to have unlimited connections
  522. var unlimitedConnections = magnet.getAttribute('unlimitedConnections');
  523. var links = state.graph.getConnectedLinks(cellView.model);
  524. for (var i = 0; i < links.length; i++)
  525. {
  526. var link = links[i];
  527. if (link.attributes.source.id === cellView.model.id && link.attributes.source.port === magnet.attributes.port.nodeValue)
  528. {
  529. // This port already has a connection
  530. if (unlimitedConnections && link.attributes.target.id)
  531. {
  532. var targetCell = state.graph.getCell(link.attributes.target.id);
  533. if (unlimitedConnections.indexOf(targetCell.attributes.type) !== -1)
  534. return true; // It's okay because this target type has unlimited connections
  535. }
  536. return false;
  537. }
  538. }
  539. return true;
  540. };
  541. func.optimized_data = function()
  542. {
  543. var cells = state.graph.toJSON().cells;
  544. var nodesByID = {};
  545. var cellsByID = {};
  546. var nodes = [];
  547. for (var i = 0; i < cells.length; i++)
  548. {
  549. var cell = cells[i];
  550. if (cell.type != 'link')
  551. {
  552. var node =
  553. {
  554. type: cell.type.slice('dialogue.'.length),
  555. id: cell.id,
  556. };
  557. if (node.type === 'Branch')
  558. {
  559. node.variable = cell.name;
  560. node.branches = {};
  561. for (var j = 0; j < cell.values.length; j++)
  562. {
  563. var branch = cell.values[j];
  564. node.branches[branch] = null;
  565. }
  566. }
  567. else if (node.type === 'Set')
  568. {
  569. node.variable = cell.name;
  570. node.value = cell.value;
  571. node.next = null;
  572. }
  573. else
  574. {
  575. node.name = cell.name;
  576. node.next = null;
  577. }
  578. if (node.type === "Choice") {
  579. node.value = cell.value;
  580. }
  581. nodes.push(node);
  582. nodesByID[cell.id] = node;
  583. cellsByID[cell.id] = cell;
  584. }
  585. }
  586. for (var i = 0; i < cells.length; i++)
  587. {
  588. var cell = cells[i];
  589. if (cell.type === 'link')
  590. {
  591. var source = nodesByID[cell.source.id];
  592. var target = cell.target ? nodesByID[cell.target.id] : null;
  593. if (source)
  594. {
  595. if (source.type === 'Branch')
  596. {
  597. var portNumber = parseInt(cell.source.port.slice('output'.length));
  598. var value;
  599. if (portNumber === 0)
  600. value = '_default';
  601. else
  602. {
  603. var sourceCell = cellsByID[source.id];
  604. value = sourceCell.values[portNumber - 1];
  605. }
  606. source.branches[value] = target ? target.id : null;
  607. }
  608. else if ((source.type === 'Text' || source.type === 'Node') && target && target.type === 'Choice')
  609. {
  610. if (!source.choices)
  611. {
  612. source.choices = [];
  613. delete source.next;
  614. }
  615. source.choices.push(target.id);
  616. }
  617. else
  618. source.next = target ? target.id : null;
  619. }
  620. }
  621. }
  622. return nodes;
  623. };
  624. // Menu actions
  625. func.flash = function(text)
  626. {
  627. var $flash = $('#flash');
  628. $flash.text(text);
  629. $flash.stop(true, true);
  630. $flash.show();
  631. $flash.css('opacity', 1.0);
  632. $flash.fadeOut({ duration: 1500 });
  633. };
  634. func.apply_fields = function()
  635. {
  636. $('input[type=text], select').blur();
  637. };
  638. func.save = function()
  639. {
  640. func.apply_fields();
  641. if (!state.filepath)
  642. func.show_save_dialog();
  643. else
  644. func.do_save();
  645. };
  646. func.optimized_filename = function(f)
  647. {
  648. return f.substring(0, f.length - 2) + 'dlz';
  649. };
  650. func.perfect_typescript = function(f)
  651. {
  652. return f.substring(0, f.length - 2) + 'dl.ts';
  653. };
  654. func.just_name = function (f) {
  655. return f.substring(0, f.length - 3);
  656. };
  657. func.do_save = function()
  658. {
  659. if (state.filepath)
  660. {
  661. fs.writeFileSync(state.filepath, JSON.stringify(state.graph), 'utf8');
  662. //fs.writeFileSync(func.optimized_filename(state.filepath), JSON.stringify(func.optimized_data()), 'utf8');
  663. let name = func.just_name(func.filename_from_filepath(state.filepath));
  664. let cleants = "// File created automatically by custom Dialogger on " + new Date().toLocaleString() + "\n";
  665. cleants += "// Do not tamper with this file.\n// It will be replaced automatically by Dialogger and all changes will be lost.\n// Instead change " + name + ".dl.";
  666. cleants += "\nmodule DialogueTrees {\n export let " + name + " = (function () {\n";
  667. cleants += func.ts_data(func.optimized_data(), name).replace(/^/gm, ' ') + "\n return tree;\n })();\n}";
  668. fs.writeFileSync(func.perfect_typescript(state.filepath), cleants, 'utf8');
  669. func.flash('Saved ' + state.filepath);
  670. }
  671. };
  672. func.filename_from_filepath = function(f)
  673. {
  674. return f.replace(/^.*[\\\/]/, '');
  675. };
  676. func.show_open_dialog = function()
  677. {
  678. $('#file_open').click();
  679. };
  680. func.show_save_dialog = function()
  681. {
  682. $('#file_save').click();
  683. };
  684. func.add_node = function(constructor)
  685. {
  686. return function()
  687. {
  688. var container = $('#container')[0];
  689. var element = new constructor(
  690. {
  691. position: { x: state.context_position.x + container.scrollLeft, y: state.context_position.y + container.scrollTop },
  692. });
  693. state.graph.addCells([element]);
  694. };
  695. };
  696. func.clear = function()
  697. {
  698. state.graph.clear();
  699. state.filepath = null;
  700. document.title = 'Dialogger';
  701. };
  702. func.handle_open_files = function(files)
  703. {
  704. state.filepath = files[0].path;
  705. var data = fs.readFileSync(state.filepath);
  706. document.title = func.filename_from_filepath(state.filepath);
  707. state.graph.clear();
  708. state.graph.fromJSON(JSON.parse(data));
  709. };
  710. func.handle_save_files = function(files)
  711. {
  712. state.filepath = files[0].path;
  713. func.do_save();
  714. };
  715. func.ts_data = function(nodes, name) {
  716. let declarations = [];
  717. let addDeclaration = (dec) => {
  718. if (declarations.indexOf(dec) == -1) {
  719. declarations.push(dec);
  720. }
  721. }
  722. addDeclaration("let tree : DialogueTree = new DialogueTree(" + JSON.stringify(name) + ");\n");
  723. let finalString = "";
  724. for (let i = 0; i < nodes.length; i++) {
  725. finalString += "\n\n";
  726. let node = nodes[i];
  727. if (node.type == "Node") {
  728. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  729. finalString += "node.setName(" + JSON.stringify(node.name) + ");\n";
  730. if (node.next != null && node.next != undefined) {
  731. finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n";
  732. }
  733. if (node.choices != undefined && node.choices != null) {
  734. finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n";
  735. }
  736. finalString += "tree.addNode(node);\n";
  737. addDeclaration("let node : DialogueNode;\n");
  738. } else if (node.type == "GoToLabel") {
  739. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  740. finalString += "node.setNext(" + JSON.stringify(node.name) + ");\n";
  741. finalString += "tree.addNode(node);\n";
  742. addDeclaration("let node : DialogueNode;\n");
  743. } else if (node.type == "Tree") {
  744. finalString += "nodetree = new DialogueNodeTree(" + JSON.stringify(node.id) + ");\n";
  745. finalString += "nodetree.setTree(() => { return " + node.name + "; });\n";
  746. if (node.next != null && node.next != undefined) {
  747. finalString += "nodetree.setNext(" + JSON.stringify(node.next) + ");\n";
  748. }
  749. if (node.choices != undefined && node.choices != null) {
  750. finalString += "nodetree.setChoices(" + JSON.stringify(node.choices) + ");\n";
  751. }
  752. finalString += "tree.addNode(nodetree);\n";
  753. addDeclaration("let nodetree : DialogueNodeTree;\n");
  754. } else if (node.type == "StartNode") {
  755. finalString += "node = new DialogueNode(" + JSON.stringify(node.id) + ");\n";
  756. if (node.next != null && node.next != undefined) {
  757. finalString += "node.setNext(" + JSON.stringify(node.next) + ");\n";
  758. }
  759. if (node.choices != undefined && node.choices != null) {
  760. finalString += "node.setChoices(" + JSON.stringify(node.choices) + ");\n";
  761. }
  762. finalString += "tree.addStartNode(node);\n";
  763. addDeclaration("let node : DialogueNode;\n");
  764. } else if (node.type == "Text") {
  765. finalString += "text = new DialogueText(" + JSON.stringify(node.id) + ");\n";
  766. //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away!
  767. // SetSay needs to be a function, otherwise data might not be updated when printing!
  768. finalString += "text.setSay(() => { return new Say(" + node.name + ");});\n";
  769. if (node.next != null && node.next != undefined) {
  770. finalString += "text.setNext(" + JSON.stringify(node.next) + ");\n";
  771. }
  772. if (node.choices != undefined && node.choices != null) {
  773. finalString += "text.setChoices(" + JSON.stringify(node.choices) + ");\n";
  774. }
  775. finalString += "tree.addNode(text);\n";
  776. addDeclaration("let text : DialogueText;\n");
  777. } else if (node.type == "Choice") {
  778. finalString += "choice = new DialogueChoice(" + JSON.stringify(node.id) + ");\n";
  779. //finalString += "node.setSay(" + node.name + ");\n"; // this way all code we placed will be added right away!
  780. // SetSay needs to be a function, otherwise data might not be updated when printing!
  781. finalString += "choice.setSay(() => { return new Say(" + node.name + ");});\n";
  782. if (node.value != undefined && node.value != null && node.value != "") {
  783. finalString += "choice.setConditions(() => { return " + node.value + ";});\n"
  784. }
  785. if (node.next != null && node.next != undefined) {
  786. finalString += "choice.setNext(" + JSON.stringify(node.next) + ");\n";
  787. }
  788. if (node.choices != undefined && node.choices != null) {
  789. finalString += "choice.setChoices(" + JSON.stringify(node.choices) + ");\n";
  790. }
  791. finalString += "tree.addNode(choice);\n";
  792. addDeclaration("let choice : DialogueChoice;\n");
  793. } else if (node.type == "Branch") {
  794. finalString += "branch = new DialogueBranch(" + JSON.stringify(node.id) + ");\n";
  795. finalString += "branch.setVariable(() => { return " + node.variable + ";});\n";
  796. for (let key in node.branches) {
  797. let branchCondition = key;
  798. let branchTarget = node.branches[key];
  799. if (branchCondition == "_default") {
  800. finalString += "branch.setNext(" + JSON.stringify(branchTarget) + ");\n";
  801. } else {
  802. finalString += "branch.addBranch(" + JSON.stringify(branchTarget) + ", () => { return " + branchCondition + ";});\n";
  803. }
  804. }
  805. finalString += "tree.addNode(branch);\n";
  806. addDeclaration("let branch : DialogueBranch;\n");
  807. } else if (node.type == "Set" || node.type == "Function") {
  808. finalString += "set = new DialogueSet(" + JSON.stringify(node.id) + ");\n";
  809. if (node.type == "Function") {
  810. finalString += "set.setFunction(() => {" + node.name + "});\n";
  811. } else {
  812. finalString += "set.setFunction(() => { " + node.variable + " = " + node.value + ";});\n";
  813. }
  814. if (node.next != null && node.next != undefined) {
  815. finalString += "set.setNext(" + JSON.stringify(node.next) + ");\n";
  816. }
  817. if (node.choices != undefined && node.choices != null) {
  818. finalString += "set.setChoices(" + JSON.stringify(node.choices) + ");\n";
  819. }
  820. finalString += "tree.addNode(set);\n";
  821. addDeclaration("let set : DialogueSet;\n");
  822. }
  823. }
  824. return declarations.join("") + finalString;
  825. };
  826. // Initialize
  827. (function()
  828. {
  829. state.paper = new joint.dia.Paper(
  830. {
  831. el: $('#paper'),
  832. width: 32000,
  833. height: 16000,
  834. model: state.graph,
  835. gridSize: 1,
  836. defaultLink: con.default_link,
  837. validateConnection: func.validate_connection,
  838. validateMagnet: func.validate_magnet,
  839. snapLinks: { radius: 75 }, // enable link snapping within 75px lookup radius
  840. markAvailable: true,
  841. });
  842. state.paper.on('blank:pointerdown', function(e, x, y)
  843. {
  844. if (e.button === 0 || e.button === 1)
  845. {
  846. state.panning = true;
  847. state.mouse_position.x = e.pageX;
  848. state.mouse_position.y = e.pageY;
  849. $('body').css('cursor', 'move');
  850. func.apply_fields();
  851. }
  852. });
  853. state.paper.on('cell:pointerdown', function(e, x, y)
  854. {
  855. func.apply_fields();
  856. });
  857. $('#container').mousemove(function(e)
  858. {
  859. if (state.panning)
  860. {
  861. var $this = $(this);
  862. $this.scrollLeft($this.scrollLeft() + state.mouse_position.x - e.pageX);
  863. $this.scrollTop($this.scrollTop() + state.mouse_position.y - e.pageY);
  864. state.mouse_position.x = e.pageX;
  865. state.mouse_position.y = e.pageY;
  866. }
  867. });
  868. $('#container').mouseup(function(e)
  869. {
  870. state.panning = false;
  871. $('body').css('cursor', 'default');
  872. });
  873. $('#file_open').on('change', function()
  874. {
  875. if (this.files)
  876. func.handle_open_files(this.files);
  877. // clear files from this input
  878. var $this = $(this);
  879. $this.wrap('<form>').parent('form').trigger('reset');
  880. $this.unwrap();
  881. });
  882. $('#file_save').on('change', function()
  883. {
  884. func.handle_save_files(this.files);
  885. });
  886. $('body').on('dragenter', function(e)
  887. {
  888. e.stopPropagation();
  889. e.preventDefault();
  890. });
  891. $('body').on('dragexit', function(e)
  892. {
  893. e.stopPropagation();
  894. e.preventDefault();
  895. });
  896. $('body').on('dragover', function(e)
  897. {
  898. e.stopPropagation();
  899. e.preventDefault();
  900. });
  901. $('body').on('drop', function(e)
  902. {
  903. e.stopPropagation();
  904. e.preventDefault();
  905. func.handle_open_files(e.originalEvent.dataTransfer.files);
  906. });
  907. $(window).on('keydown', function(event)
  908. {
  909. // Catch Ctrl-S or key code 19 on Mac (Cmd-S)
  910. if (((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') || event.which === 19)
  911. {
  912. event.stopPropagation();
  913. event.preventDefault();
  914. func.save();
  915. return false;
  916. }
  917. else if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 'o')
  918. {
  919. event.stopPropagation();
  920. event.preventDefault();
  921. func.show_open_dialog();
  922. return false;
  923. }
  924. return true;
  925. });
  926. $(window).resize(function()
  927. {
  928. func.apply_fields();
  929. var $window = $(window);
  930. var $container = $('#container');
  931. $container.height($window.innerHeight());
  932. $container.width($window.innerWidth());
  933. return this;
  934. });
  935. $(window).trigger('resize');
  936. // Context menu
  937. state.menu = new nw.Menu();
  938. state.menu.append(new nw.MenuItem({ label: 'Text', click: func.add_node(joint.shapes.dialogue.Text) }));
  939. state.menu.append(new nw.MenuItem({ label: 'Choice', click: func.add_node(joint.shapes.dialogue.Choice) }));
  940. state.menu.append(new nw.MenuItem({ label: 'Branch', click: func.add_node(joint.shapes.dialogue.Branch) }));
  941. state.menu.append(new nw.MenuItem({ label: 'Set', click: func.add_node(joint.shapes.dialogue.Set) }));
  942. state.menu.append(new nw.MenuItem({ label: 'Function', click: func.add_node(joint.shapes.dialogue.Function) }));
  943. state.menu.append(new nw.MenuItem({ label: 'GoToLabel', click: func.add_node(joint.shapes.dialogue.GoToLabel) }));
  944. state.menu.append(new nw.MenuItem({ label: 'Tree', click: func.add_node(joint.shapes.dialogue.Tree) }));
  945. state.menu.append(new nw.MenuItem({ label: 'Label', click: func.add_node(joint.shapes.dialogue.Node) }));
  946. state.menu.append(new nw.MenuItem({ label: 'Start Point', click: func.add_node(joint.shapes.dialogue.StartNode) }));
  947. state.menu.append(new nw.MenuItem({ type: 'separator' }));
  948. state.menu.append(new nw.MenuItem({ label: 'Save', click: func.save }));
  949. state.menu.append(new nw.MenuItem({ label: 'Open', click: func.show_open_dialog }));
  950. state.menu.append(new nw.MenuItem({ label: 'New', click: func.clear }));
  951. document.body.addEventListener('contextmenu', function(e)
  952. {
  953. e.preventDefault();
  954. state.context_position.x = e.x;
  955. state.context_position.y = e.y;
  956. state.menu.popup(e.x, e.y);
  957. return false;
  958. }, false);
  959. })();
  960. function updateTextareas () {
  961. window.requestAnimationFrame(updateTextareas);
  962. let textareas = new Array(...document.getElementsByTagName("textarea"));
  963. for (let i = 0; i < textareas.length; i++) {
  964. let textarea = textareas[i];
  965. textarea.style.height = "0 px";
  966. textarea.style.height = textarea.scrollHeight + "px";
  967. }
  968. }
  969. window.requestAnimationFrame(updateTextareas);