Pages.js 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202
  1. /**
  2. * Copyright (c) 2006-2016, JGraph Ltd
  3. * Copyright (c) 2006-2016, Gaudenz Alder
  4. */
  5. /**
  6. * Constructs a new point for the optional x and y coordinates. If no
  7. * coordinates are given, then the default values for <x> and <y> are used.
  8. * @constructor
  9. * @class Implements a basic 2D point. Known subclassers = {@link mxRectangle}.
  10. * @param {number} x X-coordinate of the point.
  11. * @param {number} y Y-coordinate of the point.
  12. */
  13. /**
  14. * Global types
  15. */
  16. function DiagramPage(node)
  17. {
  18. this.node = node;
  19. }
  20. /**
  21. * Holds the diagram node for the page.
  22. */
  23. DiagramPage.prototype.node = null;
  24. /**
  25. * Holds the root cell for the page.
  26. */
  27. DiagramPage.prototype.root = null;
  28. /**
  29. * Holds the view state for the page.
  30. */
  31. DiagramPage.prototype.viewState = null;
  32. /**
  33. *
  34. */
  35. DiagramPage.prototype.getName = function()
  36. {
  37. return this.node.getAttribute('name');
  38. };
  39. /**
  40. *
  41. */
  42. DiagramPage.prototype.setName = function(value)
  43. {
  44. if (value == null)
  45. {
  46. this.node.removeAttribute('name');
  47. }
  48. else
  49. {
  50. this.node.setAttribute('name', value);
  51. }
  52. };
  53. /**
  54. * Change types
  55. */
  56. function RenamePage(ui, page, name)
  57. {
  58. this.ui = ui;
  59. this.page = page;
  60. this.previous = name;
  61. }
  62. /**
  63. * Implementation of the undoable page rename.
  64. */
  65. RenamePage.prototype.execute = function()
  66. {
  67. var tmp = this.page.getName();
  68. this.page.setName(this.previous);
  69. this.previous = tmp;
  70. // Required to update page name in placeholders
  71. this.ui.editor.graph.updatePlaceholders();
  72. this.ui.editor.fireEvent(new mxEventObject('pageRenamed'));
  73. };
  74. /**
  75. * Undoable change of page title.
  76. */
  77. function MovePage(ui, oldIndex, newIndex)
  78. {
  79. this.ui = ui;
  80. this.oldIndex = oldIndex;
  81. this.newIndex = newIndex;
  82. }
  83. /**
  84. * Implementation of the undoable page rename.
  85. */
  86. MovePage.prototype.execute = function()
  87. {
  88. this.ui.pages.splice(this.newIndex, 0, this.ui.pages.splice(this.oldIndex, 1)[0]);
  89. var tmp = this.oldIndex;
  90. this.oldIndex = this.newIndex;
  91. this.newIndex = tmp;
  92. // Required to update page numbers in placeholders
  93. this.ui.editor.graph.updatePlaceholders();
  94. this.ui.editor.fireEvent(new mxEventObject('pageMoved'));
  95. };
  96. /**
  97. * Class: mxCurrentRootChange
  98. *
  99. * Action to change the current root in a view.
  100. *
  101. * Constructor: mxCurrentRootChange
  102. *
  103. * Constructs a change of the current root in the given view.
  104. */
  105. function SelectPage(ui, page)
  106. {
  107. this.ui = ui;
  108. this.page = page;
  109. this.previousPage = page;
  110. this.neverShown = this.page.viewState == null;
  111. if (page != null)
  112. {
  113. this.ui.updatePageRoot(page);
  114. }
  115. };
  116. /**
  117. * Executes selection of a new page.
  118. */
  119. SelectPage.prototype.execute = function()
  120. {
  121. var prevIndex = mxUtils.indexOf(this.ui.pages, this.previousPage);
  122. if (this.page != null && prevIndex >= 0)
  123. {
  124. var page = this.ui.currentPage;
  125. var editor = this.ui.editor;
  126. var graph = editor.graph;
  127. // Stores current diagram state in the page
  128. var data = editor.graph.compress(graph.zapGremlins(mxUtils.getXml(editor.getGraphXml(true))));
  129. mxUtils.setTextContent(page.node, data);
  130. page.viewState = graph.getViewState();
  131. page.root = graph.model.root;
  132. // Transitions for switching pages
  133. // var curIndex = mxUtils.indexOf(this.ui.pages, page);
  134. // mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transition', null);
  135. // mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transform',
  136. // (curIndex > prevIndex) ? 'translate(-50%,0)' : 'translate(50%,0)');
  137. // Removes the previous cells and clears selection
  138. graph.view.clear(page.root, true);
  139. graph.clearSelection();
  140. // Switches the current page
  141. this.ui.currentPage = this.previousPage;
  142. this.previousPage = page;
  143. page = this.ui.currentPage;
  144. // Switches the root cell and sets the view state
  145. graph.model.rootChanged(page.root);
  146. graph.setViewState(page.viewState);
  147. // Fires event to setting view state from realtime
  148. editor.fireEvent(new mxEventObject('setViewState', 'change', this));
  149. // Handles grid state in chromeless mode which is stored in Editor instance
  150. graph.gridEnabled = graph.gridEnabled && (!this.ui.editor.chromeless ||
  151. urlParams['grid'] == '1');
  152. // Updates the display
  153. editor.updateGraphComponents();
  154. graph.view.validate();
  155. graph.sizeDidChange();
  156. // mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transition', 'transform 0.2s');
  157. // mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transform', 'translate(0,0)');
  158. if (this.neverShown)
  159. {
  160. this.neverShown = false;
  161. graph.selectUnlockedLayer();
  162. }
  163. // Fires events
  164. editor.graph.fireEvent(new mxEventObject(mxEvent.ROOT));
  165. editor.fireEvent(new mxEventObject('pageSelected', 'change', this));
  166. }
  167. };
  168. /**
  169. *
  170. */
  171. function ChangePage(ui, page, select, index)
  172. {
  173. SelectPage.call(this, ui, select);
  174. this.relatedPage = page;
  175. this.index = index;
  176. this.previousIndex = null;
  177. };
  178. mxUtils.extend(ChangePage, SelectPage);
  179. /**
  180. * Function: execute
  181. *
  182. * Changes the current root of the view.
  183. */
  184. ChangePage.prototype.execute = function()
  185. {
  186. // Fires event to setting view state from realtime
  187. this.ui.editor.fireEvent(new mxEventObject('beforePageChange', 'change', this));
  188. this.previousIndex = this.index;
  189. if (this.index == null)
  190. {
  191. var tmp = mxUtils.indexOf(this.ui.pages, this.relatedPage);
  192. this.ui.pages.splice(tmp, 1);
  193. this.index = tmp;
  194. }
  195. else
  196. {
  197. this.ui.pages.splice(this.index, 0, this.relatedPage);
  198. this.index = null;
  199. }
  200. SelectPage.prototype.execute.apply(this, arguments);
  201. };
  202. /**
  203. * Returns true if the given string contains an mxfile.
  204. */
  205. EditorUi.prototype.initPages = function()
  206. {
  207. this.actions.addAction('previousPage', mxUtils.bind(this, function()
  208. {
  209. this.selectNextPage(false);
  210. }));
  211. this.actions.addAction('nextPage', mxUtils.bind(this, function()
  212. {
  213. this.selectNextPage(true);
  214. }));
  215. this.keyHandler.bindAction(33, true, 'previousPage', true); // Ctrl+Shift+PageUp
  216. this.keyHandler.bindAction(34, true, 'nextPage', true); // Ctrl+Shift+PageDown
  217. // Updates the tabs after loading the diagram
  218. var graph = this.editor.graph;
  219. var graphViewValidateBackground = graph.view.validateBackground;
  220. graph.view.validateBackground = mxUtils.bind(this, function()
  221. {
  222. if (this.tabContainer != null)
  223. {
  224. var prevHeight = this.tabContainer.style.height;
  225. if (this.fileNode == null || this.pages == null ||
  226. (this.pages.length == 1 && urlParams['pages'] == '0'))
  227. {
  228. this.tabContainer.style.height = '0px';
  229. }
  230. else
  231. {
  232. this.tabContainer.style.height = '30px';
  233. }
  234. if (prevHeight != this.tabContainer.style.height)
  235. {
  236. this.refresh(false);
  237. }
  238. }
  239. graphViewValidateBackground.apply(graph.view, arguments);
  240. });
  241. // Math workaround is only needed for initial rendering
  242. var ignorePendingMath = false;
  243. var lastPage = null;
  244. var updateTabs = mxUtils.bind(this, function()
  245. {
  246. this.updateTabContainer();
  247. // Updates scrollbar positions and backgrounds after validation
  248. var p = this.currentPage;
  249. if (p != null && p != lastPage)
  250. {
  251. if (p.viewState == null || p.viewState.scrollLeft == null)
  252. {
  253. this.resetScrollbars();
  254. if (graph.lightbox)
  255. {
  256. this.lightboxFit();
  257. }
  258. if (this.chromelessResize != null)
  259. {
  260. graph.container.scrollLeft = 0;
  261. graph.container.scrollTop = 0;
  262. this.chromelessResize();
  263. }
  264. }
  265. else
  266. {
  267. graph.container.scrollLeft = graph.view.translate.x * graph.view.scale + p.viewState.scrollLeft;
  268. graph.container.scrollTop = graph.view.translate.y * graph.view.scale + p.viewState.scrollTop;
  269. }
  270. lastPage = p;
  271. }
  272. // Updates layers window
  273. if (this.actions.layersWindow != null)
  274. {
  275. this.actions.layersWindow.refreshLayers();
  276. }
  277. // Workaround for math if tab is switched before typesetting has stopped
  278. if (typeof(MathJax) !== 'undefined' && typeof(MathJax.Hub) !== 'undefined')
  279. {
  280. // Pending math should not be rendered if the graph has no math enabled
  281. if (!ignorePendingMath)
  282. {
  283. if (MathJax.Hub.queue.pending == 1 && !this.editor.graph.mathEnabled)
  284. {
  285. // Since there is no way to stop/undo mathjax or
  286. // clear the queue, we do a refresh after typeset
  287. MathJax.Hub.Queue(mxUtils.bind(this, function()
  288. {
  289. this.editor.graph.refresh();
  290. }));
  291. }
  292. MathJax.Hub.Queue(mxUtils.bind(this, function()
  293. {
  294. ignorePendingMath = true;
  295. }));
  296. }
  297. }
  298. else if (typeof(Editor.MathJaxClear) !== 'undefined' && !this.editor.graph.mathEnabled)
  299. {
  300. // Clears our own queue for async loading
  301. ignorePendingMath = true;
  302. Editor.MathJaxClear();
  303. }
  304. });
  305. // Adds a graph model listener to update the view
  306. this.editor.graph.model.addListener(mxEvent.CHANGE, mxUtils.bind(this, function(sender, evt)
  307. {
  308. var edit = evt.getProperty('edit');
  309. var changes = edit.changes;
  310. for (var i = 0; i < changes.length; i++)
  311. {
  312. if (changes[i] instanceof SelectPage ||
  313. changes[i] instanceof RenamePage ||
  314. changes[i] instanceof MovePage ||
  315. changes[i] instanceof mxRootChange)
  316. {
  317. updateTabs();
  318. break;
  319. }
  320. }
  321. }));
  322. // Updates zoom in toolbar
  323. if (this.toolbar != null)
  324. {
  325. this.editor.addListener('pageSelected', this.toolbar.updateZoom);
  326. }
  327. };
  328. /**
  329. * Overrides setDefaultParent
  330. */
  331. Graph.prototype.createViewState = function(node)
  332. {
  333. var pv = node.getAttribute('page');
  334. var ps = node.getAttribute('pageScale');
  335. var pw = node.getAttribute('pageWidth');
  336. var ph = node.getAttribute('pageHeight');
  337. var bg = node.getAttribute('background');
  338. var temp = node.getAttribute('backgroundImage');
  339. var bgImg = (temp != null && temp.length > 0) ? JSON.parse(temp) : null;
  340. return {
  341. gridEnabled: node.getAttribute('grid') != '0',
  342. //gridColor: node.getAttribute('gridColor') || mxSettings.getGridColor(),
  343. gridSize: parseFloat(node.getAttribute('gridSize')) || mxGraph.prototype.gridSize,
  344. guidesEnabled: node.getAttribute('guides') != '0',
  345. foldingEnabled: node.getAttribute('fold') != '0',
  346. shadowVisible: node.getAttribute('shadow') == '1',
  347. pageVisible: (this.lightbox) ? false : ((pv != null) ? (pv != '0') : this.defaultPageVisible),
  348. background: (bg != null && bg.length > 0) ? bg : this.defaultGraphBackground,
  349. backgroundImage: (bgImg != null) ? new mxImage(bgImg.src, bgImg.width, bgImg.height) : null,
  350. pageScale: (ps != null) ? ps : mxGraph.prototype.pageScale,
  351. pageFormat: (pw != null && ph != null) ? new mxRectangle(0, 0,
  352. parseFloat(pw), parseFloat(ph)) : this.pageFormat,
  353. tooltips: node.getAttribute('tooltips') != '0',
  354. connect: node.getAttribute('connect') != '0',
  355. arrows: node.getAttribute('arrows') != '0',
  356. mathEnabled: node.getAttribute('math') != '0',
  357. selectionCells: null,
  358. defaultParent: null,
  359. scrollbars: this.defaultScrollbars,
  360. scale: 1
  361. };
  362. };
  363. /**
  364. * Overrides setDefaultParent
  365. */
  366. Graph.prototype.getViewState = function()
  367. {
  368. return {
  369. defaultParent: this.defaultParent,
  370. currentRoot: this.view.currentRoot,
  371. gridEnabled: this.gridEnabled,
  372. //gridColor: this.view.gridColor,
  373. gridSize: this.gridSize,
  374. guidesEnabled: this.graphHandler.guidesEnabled,
  375. foldingEnabled: this.foldingEnabled,
  376. shadowVisible: this.shadowVisible,
  377. scrollbars: this.scrollbars,
  378. pageVisible: this.pageVisible,
  379. background: this.background,
  380. backgroundImage: this.backgroundImage,
  381. pageScale: this.pageScale,
  382. pageFormat: this.pageFormat,
  383. tooltips: this.tooltipHandler.isEnabled(),
  384. connect: this.connectionHandler.isEnabled(),
  385. arrows: this.connectionArrowsEnabled,
  386. scale: this.view.scale,
  387. scrollLeft: this.container.scrollLeft - this.view.translate.x * this.view.scale,
  388. scrollTop: this.container.scrollTop - this.view.translate.y * this.view.scale,
  389. translate: this.view.translate.clone(),
  390. lastPasteXml: this.lastPasteXml,
  391. pasteCounter: this.pasteCounter,
  392. mathEnabled: this.mathEnabled
  393. };
  394. };
  395. /**
  396. * Overrides setDefaultParent
  397. */
  398. Graph.prototype.setViewState = function(state)
  399. {
  400. if (state != null)
  401. {
  402. this.lastPasteXml = state.lastPasteXml;
  403. this.pasteCounter = state.pasteCounter || 0;
  404. this.mathEnabled = state.mathEnabled;
  405. this.gridEnabled = state.gridEnabled;
  406. //this.view.gridColor = state.gridColor;
  407. this.gridSize = state.gridSize;
  408. this.graphHandler.guidesEnabled = state.guidesEnabled;
  409. this.foldingEnabled = state.foldingEnabled;
  410. this.setShadowVisible(state.shadowVisible, false);
  411. this.scrollbars = state.scrollbars;
  412. this.pageVisible = state.pageVisible;
  413. this.background = state.background;
  414. this.backgroundImage = state.backgroundImage;
  415. this.pageScale = state.pageScale;
  416. this.pageFormat = state.pageFormat;
  417. this.view.scale = state.scale;
  418. this.view.currentRoot = state.currentRoot;
  419. this.defaultParent = state.defaultParent;
  420. this.connectionArrowsEnabled = state.arrows;
  421. this.setTooltips(state.tooltips);
  422. this.setConnectable(state.connect);
  423. // Checks if current root or default parent have been removed
  424. if (!this.model.contains(this.view.currentRoot))
  425. {
  426. this.view.currentRoot = null;
  427. }
  428. if (!this.model.contains(this.defaultParent))
  429. {
  430. this.setDefaultParent(null);
  431. this.selectUnlockedLayer();
  432. }
  433. if (state.translate != null)
  434. {
  435. this.view.translate = state.translate;
  436. }
  437. }
  438. else
  439. {
  440. this.view.currentRoot = null;
  441. this.view.scale = 1;
  442. this.gridEnabled = true;
  443. this.gridSize = mxGraph.prototype.gridSize;
  444. this.pageScale = mxGraph.prototype.pageScale;
  445. this.pageFormat = mxSettings.getPageFormat();
  446. this.pageVisible = this.defaultPageVisible;
  447. this.background = this.defaultGraphBackground;
  448. this.backgroundImage = null;
  449. this.scrollbars = this.defaultScrollbars;
  450. this.graphHandler.guidesEnabled = true;
  451. this.foldingEnabled = true;
  452. this.defaultParent = null;
  453. this.setTooltips(true);
  454. this.setConnectable(true);
  455. this.lastPasteXml = null;
  456. this.pasteCounter = 0;
  457. this.mathEnabled = false;
  458. this.connectionArrowsEnabled = true;
  459. }
  460. // Implicit settings
  461. this.pageBreaksVisible = this.pageVisible;
  462. this.preferPageSize = this.pageVisible;
  463. };
  464. /**
  465. * Executes selection of a new page.
  466. */
  467. EditorUi.prototype.updatePageRoot = function(page)
  468. {
  469. if (page.root == null)
  470. {
  471. var node = this.editor.extractGraphModel(page.node);
  472. if (node != null)
  473. {
  474. page.graphModelNode = node;
  475. // Converts model XML into page object with root cell
  476. page.viewState = this.editor.graph.createViewState(node);
  477. var codec = new mxCodec(node.ownerDocument);
  478. page.root = codec.decode(node).root;
  479. }
  480. else
  481. {
  482. // Initializes page object with new empty root
  483. page.root = this.editor.graph.model.createRoot();
  484. }
  485. }
  486. return page;
  487. };
  488. /**
  489. * Returns true if the given string contains an mxfile.
  490. */
  491. EditorUi.prototype.selectPage = function(page)
  492. {
  493. this.editor.graph.stopEditing();
  494. var edit = this.editor.graph.model.createUndoableEdit();
  495. // Special flag to bypass autosave for this edit
  496. edit.ignoreEdit = true;
  497. var change = new SelectPage(this, page);
  498. change.execute();
  499. edit.add(change);
  500. edit.notify();
  501. this.editor.graph.model.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
  502. };
  503. /**
  504. *
  505. */
  506. EditorUi.prototype.selectNextPage = function(forward)
  507. {
  508. var next = this.currentPage;
  509. if (next != null && this.pages != null)
  510. {
  511. var tmp = mxUtils.indexOf(this.pages, next);
  512. if (forward)
  513. {
  514. this.selectPage(this.pages[mxUtils.mod(tmp + 1, this.pages.length)]);
  515. }
  516. else if (!forward)
  517. {
  518. this.selectPage(this.pages[mxUtils.mod(tmp - 1, this.pages.length)]);
  519. }
  520. }
  521. };
  522. /**
  523. * Returns true if the given string contains an mxfile.
  524. */
  525. EditorUi.prototype.insertPage = function(page, index)
  526. {
  527. if (this.editor.graph.isEnabled())
  528. {
  529. page = (page != null) ? page : this.createPage();
  530. index = (index != null) ? index : this.pages.length;
  531. // Uses model to fire event and trigger autosave
  532. var change = new ChangePage(this, page, page, index);
  533. this.editor.graph.model.execute(change);
  534. }
  535. return page;
  536. };
  537. /**
  538. * Returns true if the given string contains an mxfile.
  539. */
  540. EditorUi.prototype.createPage = function(name)
  541. {
  542. var page = new DiagramPage(this.fileNode.ownerDocument.createElement('diagram'));
  543. page.setName((name != null) ? name : this.createPageName());
  544. return page;
  545. };
  546. /**
  547. * Returns true if the given string contains an mxfile.
  548. */
  549. EditorUi.prototype.createPageName = function()
  550. {
  551. // Creates a lookup with names
  552. var existing = {};
  553. for (var i = 0; i < this.pages.length; i++)
  554. {
  555. var tmp = this.pages[i].getName();
  556. if (tmp != null && tmp.length > 0)
  557. {
  558. existing[tmp] = tmp;
  559. }
  560. }
  561. // Avoids existing names
  562. var nr = this.pages.length;
  563. var name = null;
  564. do
  565. {
  566. name = mxResources.get('pageWithNumber', [++nr]);
  567. }
  568. while (existing[name] != null);
  569. return name;
  570. };
  571. /**
  572. * Returns true if the given string contains an mxfile.
  573. */
  574. EditorUi.prototype.removePage = function(page)
  575. {
  576. var graph = this.editor.graph;
  577. if (graph.isEnabled())
  578. {
  579. graph.model.beginUpdate();
  580. try
  581. {
  582. var next = this.currentPage;
  583. if (next == page)
  584. {
  585. if (this.pages.length > 1)
  586. {
  587. var tmp = mxUtils.indexOf(this.pages, page);
  588. if (tmp == this.pages.length - 1)
  589. {
  590. tmp--;
  591. }
  592. else
  593. {
  594. tmp++;
  595. }
  596. next = this.pages[tmp];
  597. }
  598. else
  599. {
  600. // Removes label with incorrect page number to force
  601. // default page name which is OK for a single page
  602. next = this.insertPage();
  603. graph.model.execute(new RenamePage(this, next, mxResources.get('pageWithNumber', [1])));
  604. }
  605. }
  606. // Uses model to fire event to trigger autosave
  607. graph.model.execute(new ChangePage(this, page, next));
  608. }
  609. finally
  610. {
  611. graph.model.endUpdate();
  612. }
  613. }
  614. return page;
  615. };
  616. /**
  617. * Returns true if the given string contains an mxfile.
  618. */
  619. EditorUi.prototype.duplicatePage = function(page, name)
  620. {
  621. var graph = this.editor.graph;
  622. var newPage = null;
  623. if (graph.isEnabled())
  624. {
  625. // Clones the current page and takes a snapshot of the graph model and view state
  626. var newPage = new DiagramPage(page.node.cloneNode(false));
  627. newPage.root = graph.cloneCells([graph.model.root])[0];
  628. newPage.viewState = graph.getViewState();
  629. // Resets zoom and scrollbar positions
  630. newPage.viewState.scale = 1;
  631. newPage.viewState.scrollLeft = null;
  632. newPage.viewState.scrollRight = null;
  633. newPage.setName(name);
  634. newPage = this.insertPage(newPage, mxUtils.indexOf(this.pages, page) + 1);
  635. }
  636. return newPage;
  637. };
  638. /**
  639. * Returns true if the given string contains an mxfile.
  640. */
  641. EditorUi.prototype.renamePage = function(page)
  642. {
  643. var graph = this.editor.graph;
  644. if (graph.isEnabled())
  645. {
  646. var dlg = new FilenameDialog(this, page.getName(), mxResources.get('rename'), mxUtils.bind(this, function(name)
  647. {
  648. if (name != null && name.length > 0)
  649. {
  650. this.editor.graph.model.execute(new RenamePage(this, page, name));
  651. }
  652. }), mxResources.get('rename'));
  653. this.showDialog(dlg.container, 300, 80, true, true);
  654. dlg.init();
  655. }
  656. return page;
  657. }
  658. /**
  659. * Returns true if the given string contains an mxfile.
  660. */
  661. EditorUi.prototype.movePage = function(oldIndex, newIndex)
  662. {
  663. this.editor.graph.model.execute(new MovePage(this, oldIndex, newIndex));
  664. }
  665. /**
  666. * Returns true if the given string contains an mxfile.
  667. */
  668. EditorUi.prototype.createTabContainer = function()
  669. {
  670. var div = document.createElement('div');
  671. div.style.backgroundColor = '#dcdcdc';
  672. div.style.position = 'absolute';
  673. div.style.whiteSpace = 'nowrap';
  674. div.style.overflow = 'hidden';
  675. div.style.height = '0px';
  676. return div;
  677. };
  678. /**
  679. * Returns true if the given string contains an mxfile.
  680. */
  681. EditorUi.prototype.updateTabContainer = function()
  682. {
  683. if (this.tabContainer != null && this.pages != null)
  684. {
  685. var graph = this.editor.graph;
  686. var wrapper = document.createElement('div');
  687. wrapper.style.position = 'relative';
  688. wrapper.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
  689. wrapper.style.verticalAlign = 'top';
  690. wrapper.style.height = this.tabContainer.style.height;
  691. wrapper.style.whiteSpace = 'nowrap';
  692. wrapper.style.overflow = 'hidden';
  693. wrapper.style.fontSize = '12px';
  694. // Allows for negative left margin of first tab
  695. wrapper.style.marginLeft = '30px';
  696. // Automatic tab width to match available width
  697. // TODO: Fix tabWidth in chromeless mode
  698. var btnWidth = (this.editor.chromeless) ? 29 : 59;
  699. var tabWidth = Math.min(140, Math.max(20, (this.tabContainer.clientWidth - btnWidth) / this.pages.length) + 1);
  700. var startIndex = null;
  701. for (var i = 0; i < this.pages.length; i++)
  702. {
  703. // Install drag and drop for page reorder
  704. (mxUtils.bind(this, function(index, tab)
  705. {
  706. if (this.pages[index] == this.currentPage)
  707. {
  708. tab.style.backgroundColor = '#eeeeee';
  709. tab.style.fontWeight = 'bold';
  710. tab.style.borderTopStyle = 'none';
  711. }
  712. tab.setAttribute('draggable', 'true');
  713. mxEvent.addListener(tab, 'dragstart', mxUtils.bind(this, function(evt)
  714. {
  715. if (graph.isEnabled())
  716. {
  717. startIndex = index;
  718. }
  719. else
  720. {
  721. // Blocks event
  722. mxEvent.consume(evt);
  723. }
  724. }));
  725. mxEvent.addListener(tab, 'dragend', mxUtils.bind(this, function(evt)
  726. {
  727. startIndex = null;
  728. evt.stopPropagation();
  729. evt.preventDefault();
  730. }));
  731. mxEvent.addListener(tab, 'dragover', mxUtils.bind(this, function(evt)
  732. {
  733. if (startIndex != null)
  734. {
  735. evt.dataTransfer.dropEffect = 'move';
  736. }
  737. evt.stopPropagation();
  738. evt.preventDefault();
  739. }));
  740. mxEvent.addListener(tab, 'drop', mxUtils.bind(this, function(evt)
  741. {
  742. if (startIndex != null && index != startIndex)
  743. {
  744. // TODO: Shift drag for insert/merge?
  745. this.movePage(startIndex, index);
  746. }
  747. evt.stopPropagation();
  748. evt.preventDefault();
  749. }));
  750. wrapper.appendChild(tab);
  751. }))(i, this.createTabForPage(this.pages[i], tabWidth, this.pages[i] != this.currentPage));
  752. }
  753. this.tabContainer.innerHTML = '';
  754. this.tabContainer.appendChild(wrapper);
  755. // Adds floating menu with all pages and insert option
  756. var menutab = this.createPageMenuTab();
  757. this.tabContainer.appendChild(menutab);
  758. var insertTab = null;
  759. // Not chromeless and not read-only file
  760. if (graph.isEnabled())
  761. {
  762. insertTab = this.createPageInsertTab();
  763. this.tabContainer.appendChild(insertTab);
  764. }
  765. if (wrapper.clientWidth > this.tabContainer.clientWidth - btnWidth)
  766. {
  767. if (insertTab != null)
  768. {
  769. insertTab.style.position = 'absolute';
  770. insertTab.style.right = '0px';
  771. wrapper.style.marginRight = '30px';
  772. }
  773. var temp = this.createControlTab(4, '&nbsp;&#10094;&nbsp;');
  774. temp.style.position = 'absolute';
  775. temp.style.right = (this.editor.chromeless) ? '29px' : '55px';
  776. temp.style.fontSize = '13pt';
  777. this.tabContainer.appendChild(temp);
  778. var temp2 = this.createControlTab(4, '&nbsp;&#10095;');
  779. temp2.style.position = 'absolute';
  780. temp2.style.right = (this.editor.chromeless) ? '0px' : '29px';
  781. temp2.style.fontSize = '13pt';
  782. this.tabContainer.appendChild(temp2);
  783. // TODO: Scroll to current page
  784. var dx = Math.max(0, this.tabContainer.clientWidth - ((this.editor.chromeless) ? 86 : 116));
  785. wrapper.style.width = dx + 'px';
  786. var fade = 50;
  787. mxEvent.addListener(temp, 'click', mxUtils.bind(this, function(evt)
  788. {
  789. wrapper.scrollLeft -= Math.max(20, dx - 20);
  790. mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade);
  791. mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade);
  792. mxEvent.consume(evt);
  793. }));
  794. mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade);
  795. mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade);
  796. mxEvent.addListener(temp2, 'click', mxUtils.bind(this, function(evt)
  797. {
  798. wrapper.scrollLeft += Math.max(20, dx - 20);
  799. mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade);
  800. mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade);
  801. mxEvent.consume(evt);
  802. }));
  803. }
  804. }
  805. };
  806. /**
  807. * Returns true if the given string contains an mxfile.
  808. */
  809. EditorUi.prototype.createTab = function(hoverEnabled)
  810. {
  811. var tab = document.createElement('div');
  812. tab.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
  813. tab.style.whiteSpace = 'nowrap';
  814. tab.style.boxSizing = 'border-box';
  815. tab.style.position = 'relative';
  816. tab.style.overflow = 'hidden';
  817. tab.style.marginLeft = '-1px';
  818. tab.style.height = this.tabContainer.clientHeight + 'px';
  819. tab.style.padding = '8px 4px 8px 4px';
  820. tab.style.border = '1px solid #c0c0c0';
  821. tab.style.borderBottomStyle = 'solid';
  822. tab.style.backgroundColor = this.tabContainer.style.backgroundColor;
  823. tab.style.cursor = 'default';
  824. tab.style.color = 'gray';
  825. if (hoverEnabled)
  826. {
  827. mxEvent.addListener(tab, 'mouseenter', mxUtils.bind(this, function(evt)
  828. {
  829. if (!this.editor.graph.isMouseDown)
  830. {
  831. tab.style.backgroundColor = '#d3d3d3';
  832. mxEvent.consume(evt);
  833. }
  834. }));
  835. mxEvent.addListener(tab, 'mouseleave', mxUtils.bind(this, function(evt)
  836. {
  837. tab.style.backgroundColor = this.tabContainer.style.backgroundColor;
  838. mxEvent.consume(evt);
  839. }));
  840. }
  841. return tab;
  842. };
  843. /**
  844. * Returns true if the given string contains an mxfile.
  845. */
  846. EditorUi.prototype.createControlTab = function(paddingTop, html)
  847. {
  848. var tab = this.createTab(true);
  849. tab.style.paddingTop = paddingTop + 'px';
  850. tab.style.cursor = 'pointer';
  851. tab.style.width = '30px';
  852. tab.style.lineHeight = '30px';
  853. tab.innerHTML = html;
  854. if (tab.firstChild != null && tab.firstChild.style != null)
  855. {
  856. mxUtils.setOpacity(tab.firstChild, 40);
  857. }
  858. return tab;
  859. };
  860. /**
  861. * Returns true if the given string contains an mxfile.
  862. */
  863. EditorUi.prototype.createPageMenuTab = function()
  864. {
  865. var tab = this.createControlTab(3, '<div class="geSprite geSprite-dots" style="display:inline-block;width:21px;height:21px;"></div>');
  866. tab.setAttribute('title', mxResources.get('pages'));
  867. tab.style.position = 'absolute';
  868. tab.style.left = '1px';
  869. mxEvent.addListener(tab, 'click', mxUtils.bind(this, function(evt)
  870. {
  871. this.editor.graph.popupMenuHandler.hideMenu();
  872. var menu = new mxPopupMenu(mxUtils.bind(this, function(menu, parent)
  873. {
  874. for (var i = 0; i < this.pages.length; i++)
  875. {
  876. (mxUtils.bind(this, function(index)
  877. {
  878. var item = menu.addItem(this.pages[index].getName(), null, mxUtils.bind(this, function()
  879. {
  880. this.selectPage(this.pages[index]);
  881. }), parent);
  882. // Adds checkmark to current page
  883. if (this.pages[index] == this.currentPage)
  884. {
  885. menu.addCheckmark(item, Editor.checkmarkImage);
  886. }
  887. }))(i);
  888. }
  889. if (this.editor.graph.isEnabled())
  890. {
  891. menu.addSeparator(parent);
  892. var item = menu.addItem(mxResources.get('insertPage'), null, mxUtils.bind(this, function()
  893. {
  894. this.insertPage();
  895. }), parent);
  896. }
  897. }));
  898. menu.div.className += ' geMenubarMenu';
  899. menu.smartSeparators = true;
  900. menu.showDisabled = true;
  901. menu.autoExpand = true;
  902. // Disables autoexpand and destroys menu when hidden
  903. menu.hideMenu = mxUtils.bind(this, function()
  904. {
  905. mxPopupMenu.prototype.hideMenu.apply(menu, arguments);
  906. menu.destroy();
  907. });
  908. var x = mxEvent.getClientX(evt);
  909. var y = mxEvent.getClientY(evt);
  910. menu.popup(x, y, null, evt);
  911. // Allows hiding by clicking on document
  912. this.setCurrentMenu(menu);
  913. mxEvent.consume(evt);
  914. }));
  915. return tab;
  916. };
  917. /**
  918. * Returns true if the given string contains an mxfile.
  919. */
  920. EditorUi.prototype.createPageInsertTab = function()
  921. {
  922. var tab = this.createControlTab(4, '<div class="geSprite geSprite-plus" style="display:inline-block;width:21px;height:21px;"></div>');
  923. tab.setAttribute('title', mxResources.get('insertPage'));
  924. var graph = this.editor.graph;
  925. mxEvent.addListener(tab, 'click', mxUtils.bind(this, function(evt)
  926. {
  927. this.insertPage();
  928. mxEvent.consume(evt);
  929. }));
  930. return tab;
  931. };
  932. /**
  933. * Returns true if the given string contains an mxfile.
  934. */
  935. EditorUi.prototype.createTabForPage = function(page, tabWidth, hoverEnabled)
  936. {
  937. var tab = this.createTab(hoverEnabled);
  938. var name = page.getName();
  939. tab.setAttribute('title', name);
  940. mxUtils.write(tab, name);
  941. tab.style.maxWidth = tabWidth + 'px';
  942. tab.style.width = tabWidth + 'px';
  943. this.addTabListeners(page, tab);
  944. if (tabWidth > 42)
  945. {
  946. tab.style.textOverflow = 'ellipsis';
  947. }
  948. return tab;
  949. };
  950. /**
  951. * Translates this point by the given vector.
  952. *
  953. * @param {number} dx X-coordinate of the translation.
  954. * @param {number} dy Y-coordinate of the translation.
  955. */
  956. EditorUi.prototype.addTabListeners = function(page, tab)
  957. {
  958. mxEvent.disableContextMenu(tab);
  959. var graph = this.editor.graph;
  960. var model = graph.model;
  961. mxEvent.addListener(tab, 'dblclick', mxUtils.bind(this, function(evt)
  962. {
  963. this.renamePage(page)
  964. mxEvent.consume(evt);
  965. }));
  966. var menuWasVisible = false;
  967. var pageWasActive = false;
  968. mxEvent.addGestureListeners(tab, mxUtils.bind(this, function(evt)
  969. {
  970. // Do not consume event here to allow for drag and drop of tabs
  971. menuWasVisible = this.currentMenu != null;
  972. pageWasActive = page == this.currentPage;
  973. if (!graph.isMouseDown && !pageWasActive)
  974. {
  975. this.selectPage(page);
  976. }
  977. }), null, mxUtils.bind(this, function(evt)
  978. {
  979. if (graph.isEnabled() && !graph.isMouseDown &&
  980. ((mxEvent.isTouchEvent(evt) && pageWasActive) ||
  981. mxEvent.isPopupTrigger(evt)))
  982. {
  983. graph.popupMenuHandler.hideMenu();
  984. this.hideCurrentMenu();
  985. if (!mxEvent.isTouchEvent(evt) || !menuWasVisible)
  986. {
  987. var menu = new mxPopupMenu(this.createPageMenu(page));
  988. menu.div.className += ' geMenubarMenu';
  989. menu.smartSeparators = true;
  990. menu.showDisabled = true;
  991. menu.autoExpand = true;
  992. // Disables autoexpand and destroys menu when hidden
  993. menu.hideMenu = mxUtils.bind(this, function()
  994. {
  995. mxPopupMenu.prototype.hideMenu.apply(menu, arguments);
  996. this.resetCurrentMenu();
  997. menu.destroy();
  998. });
  999. var x = mxEvent.getClientX(evt);
  1000. var y = mxEvent.getClientY(evt);
  1001. menu.popup(x, y, null, evt);
  1002. this.setCurrentMenu(menu, tab);
  1003. }
  1004. mxEvent.consume(evt);
  1005. }
  1006. }));
  1007. };
  1008. /**
  1009. * Returns true if the given string contains an mxfile.
  1010. */
  1011. EditorUi.prototype.createPageMenu = function(page, label)
  1012. {
  1013. return mxUtils.bind(this, function(menu, parent)
  1014. {
  1015. var graph = this.editor.graph;
  1016. var model = graph.model;
  1017. menu.addItem(mxResources.get('insert'), null, mxUtils.bind(this, function()
  1018. {
  1019. this.insertPage(null, mxUtils.indexOf(this.pages, page) + 1);
  1020. }), parent);
  1021. menu.addItem(mxResources.get('delete'), null, mxUtils.bind(this, function()
  1022. {
  1023. this.removePage(page);
  1024. }), parent);
  1025. menu.addItem(mxResources.get('rename'), null, mxUtils.bind(this, function()
  1026. {
  1027. this.renamePage(page, label);
  1028. }), parent);
  1029. menu.addSeparator(parent);
  1030. menu.addItem(mxResources.get('duplicate'), null, mxUtils.bind(this, function()
  1031. {
  1032. this.duplicatePage(page, mxResources.get('copyOf', [page.getName()]));
  1033. }), parent);
  1034. });
  1035. };