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 */
16function DiagramPage(node, id)
17{
18	this.node = node;
19
20	if (id != null)
21	{
22		this.node.setAttribute('id', id);
23	}
24	else if (this.getId() == null)
25	{
26		this.node.setAttribute('id', Editor.guid());
27	}
28};
29
30/**
31 * Holds the diagram node for the page.
32 */
33DiagramPage.prototype.node = null;
34
35/**
36 * Holds the root cell for the page.
37 */
38DiagramPage.prototype.root = null;
39
40/**
41 * Holds the view state for the page.
42 */
43DiagramPage.prototype.viewState = null;
44
45/**
46 *
47 */
48DiagramPage.prototype.getId = function()
49{
50	return this.node.getAttribute('id');
51};
52
53/**
54 *
55 */
56DiagramPage.prototype.getName = function()
57{
58	return this.node.getAttribute('name');
59};
60
61/**
62 *
63 */
64DiagramPage.prototype.setName = function(value)
65{
66	if (value == null)
67	{
68		this.node.removeAttribute('name');
69	}
70	else
71	{
72		this.node.setAttribute('name', value);
73	}
74};
75
76/**
77 * Change types
78 */
79function RenamePage(ui, page, name)
80{
81	this.ui = ui;
82	this.page = page;
83	this.name = name;
84	this.previous = name;
85}
86
87/**
88 * Implementation of the undoable page rename.
89 */
90RenamePage.prototype.execute = function()
91{
92	var tmp = this.page.getName();
93	this.page.setName(this.previous);
94	this.name = this.previous;
95	this.previous = tmp;
96
97	// Required to update page name in placeholders
98	this.ui.editor.graph.updatePlaceholders();
99	this.ui.editor.fireEvent(new mxEventObject('pageRenamed'));
100};
101
102/**
103 * Undoable change of page title.
104 */
105function MovePage(ui, oldIndex, newIndex)
106{
107	this.ui = ui;
108	this.oldIndex = oldIndex;
109	this.newIndex = newIndex;
110}
111
112/**
113 * Implementation of the undoable page rename.
114 */
115MovePage.prototype.execute = function()
116{
117	this.ui.pages.splice(this.newIndex, 0, this.ui.pages.splice(this.oldIndex, 1)[0]);
118	var tmp = this.oldIndex;
119	this.oldIndex = this.newIndex;
120	this.newIndex = tmp;
121
122	// Required to update page numbers in placeholders
123	this.ui.editor.graph.updatePlaceholders();
124	this.ui.editor.fireEvent(new mxEventObject('pageMoved'));
125};
126
127/**
128 * Class: mxCurrentRootChange
129 *
130 * Action to change the current root in a view.
131 *
132 * Constructor: mxCurrentRootChange
133 *
134 * Constructs a change of the current root in the given view.
135 */
136function SelectPage(ui, page, viewState)
137{
138	this.ui = ui;
139	this.page = page;
140	this.previousPage = page;
141	this.neverShown = true;
142
143	if (page != null)
144	{
145		this.neverShown = page.viewState == null;
146		this.ui.updatePageRoot(page);
147
148		if (viewState != null)
149		{
150			page.viewState = viewState;
151			this.neverShown = false;
152		}
153	}
154};
155
156/**
157 * Executes selection of a new page.
158 */
159SelectPage.prototype.execute = function()
160{
161	var prevIndex = mxUtils.indexOf(this.ui.pages, this.previousPage);
162
163	if (this.page != null && prevIndex >= 0)
164	{
165		var page = this.ui.currentPage;
166		var editor = this.ui.editor;
167		var graph = editor.graph;
168
169		// Stores current diagram state in the page
170		var data = Graph.compressNode(editor.getGraphXml(true));
171		mxUtils.setTextContent(page.node, data);
172		page.viewState = graph.getViewState();
173		page.root = graph.model.root;
174
175		if (page.model != null)
176		{
177			// Updates internal structures of offpage model
178			page.model.rootChanged(page.root);
179		}
180
181		// Transitions for switching pages
182//		var curIndex = mxUtils.indexOf(this.ui.pages, page);
183//		mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transition', null);
184//		mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transform',
185//			(curIndex > prevIndex) ? 'translate(-50%,0)' : 'translate(50%,0)');
186
187		// Removes the previous cells and clears selection
188		graph.view.clear(page.root, true);
189		graph.clearSelection();
190
191		// Switches the current page
192		this.ui.currentPage = this.previousPage;
193		this.previousPage = page;
194		page = this.ui.currentPage;
195
196		// Switches the root cell and sets the view state
197		graph.model.prefix = Editor.guid() + '-';
198		graph.model.rootChanged(page.root);
199		graph.setViewState(page.viewState);
200
201		// Handles grid state in chromeless mode which is stored in Editor instance
202		graph.gridEnabled = graph.gridEnabled && (!this.ui.editor.isChromelessView() ||
203			urlParams['grid'] == '1');
204
205		// Updates the display
206		editor.updateGraphComponents();
207		graph.view.validate();
208		graph.blockMathRender = true;
209		graph.sizeDidChange();
210		graph.blockMathRender = false;
211
212//		mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transition', 'transform 0.2s');
213//		mxUtils.setPrefixedStyle(graph.view.canvas.style, 'transform', 'translate(0,0)');
214
215		if (this.neverShown)
216		{
217			this.neverShown = false;
218			graph.selectUnlockedLayer();
219		}
220
221		// Fires events
222		editor.graph.fireEvent(new mxEventObject(mxEvent.ROOT));
223		editor.fireEvent(new mxEventObject('pageSelected', 'change', this));
224	}
225};
226
227/**
228 *
229 */
230function ChangePage(ui, page, select, index, noSelect)
231{
232	SelectPage.call(this, ui, select);
233	this.relatedPage = page;
234	this.index = index;
235	this.previousIndex = null;
236	this.noSelect = noSelect;
237};
238
239mxUtils.extend(ChangePage, SelectPage);
240
241/**
242 * Function: execute
243 *
244 * Changes the current root of the view.
245 */
246ChangePage.prototype.execute = function()
247{
248	// Fires event to setting view state from realtime
249	this.ui.editor.fireEvent(new mxEventObject('beforePageChange', 'change', this));
250	this.previousIndex = this.index;
251
252	if (this.index == null)
253	{
254		var tmp = mxUtils.indexOf(this.ui.pages, this.relatedPage);
255		this.ui.pages.splice(tmp, 1);
256		this.index = tmp;
257	}
258	else
259	{
260		this.ui.pages.splice(this.index, 0, this.relatedPage);
261		this.index = null;
262	}
263
264	if (!this.noSelect)
265	{
266		SelectPage.prototype.execute.apply(this, arguments);
267	}
268};
269
270/**
271 * Specifies the height of the tab container. Default is 38.
272 */
273EditorUi.prototype.tabContainerHeight = 38;
274
275/**
276 * Returns the index of the selected page.
277 */
278EditorUi.prototype.getSelectedPageIndex = function()
279{
280	return this.getPageIndex(this.currentPage);
281};
282
283/**
284 * Returns the index of the given page.
285 */
286 EditorUi.prototype.getPageIndex = function(page)
287 {
288	 var result = null;
289
290	 if (this.pages != null && page != null)
291	 {
292		 for (var i = 0; i < this.pages.length; i++)
293		 {
294			 if (this.pages[i] == page)
295			 {
296				 result = i;
297
298				 break;
299			 }
300		 }
301	 }
302
303	 return result;
304 };
305
306/**
307 * Returns true if the given string contains an mxfile.
308 */
309EditorUi.prototype.getPageById = function(id)
310{
311	if (this.pages != null)
312	{
313		for (var i = 0; i < this.pages.length; i++)
314		{
315			if (this.pages[i].getId() == id)
316			{
317				return this.pages[i];
318			}
319		}
320	}
321
322	return null;
323};
324
325/**
326 * Returns the background image for the given page link.
327 */
328EditorUi.prototype.createImageForPageLink = function(src, sourcePage, sourceGraph)
329{
330	var comma = src.indexOf(',');
331	var result = null;
332
333	if (comma > 0)
334	{
335		var page = this.getPageById(src.substring(comma + 1));
336
337		if (page != null && page != sourcePage)
338		{
339			result = this.getImageForPage(page, sourcePage, sourceGraph);
340			result.originalSrc = src;
341		}
342	}
343
344	if (result == null)
345	{
346		result = {originalSrc: src};
347	}
348
349	return result;
350};
351
352/**
353 * Returns true if the given string contains an mxfile.
354 */
355EditorUi.prototype.getImageForPage = function(page, sourcePage, sourceGraph)
356{
357	sourceGraph = (sourceGraph != null) ? sourceGraph : this.editor.graph;
358	var graphGetGlobalVariable = sourceGraph.getGlobalVariable;
359	var graph = this.createTemporaryGraph(sourceGraph.getStylesheet());
360	graph.defaultPageBackgroundColor = sourceGraph.defaultPageBackgroundColor;
361	graph.shapeBackgroundColor = sourceGraph.shapeBackgroundColor;
362	graph.shapeForegroundColor = sourceGraph.shapeForegroundColor;
363	var index = this.getPageIndex((sourcePage != null) ?
364		sourcePage : this.currentPage);
365
366	graph.getGlobalVariable = function(name)
367	{
368		if (name == 'pagenumber')
369		{
370			return index + 1;
371		}
372		else if (name == 'page' && sourcePage != null)
373		{
374			return sourcePage.getName();
375		}
376		else
377		{
378			return graphGetGlobalVariable.apply(this, arguments);
379		}
380	};
381
382	document.body.appendChild(graph.container);
383
384	this.updatePageRoot(page);
385	graph.model.setRoot(page.root);
386	var svgRoot = graph.getSvg(null, null, null, null, null,
387		null, null, null, null, null, null, true);
388	var bounds = graph.getGraphBounds();
389	document.body.removeChild(graph.container);
390
391	return new mxImage(Editor.createSvgDataUri(mxUtils.getXml(svgRoot)),
392		bounds.width, bounds.height, bounds.x, bounds.y);
393};
394
395/**
396 * Returns true if the given string contains an mxfile.
397 */
398EditorUi.prototype.initPages = function()
399{
400	if (!this.editor.graph.standalone)
401	{
402		this.actions.addAction('previousPage', mxUtils.bind(this, function()
403		{
404			this.selectNextPage(false);
405		}));
406
407		this.actions.addAction('nextPage', mxUtils.bind(this, function()
408		{
409			this.selectNextPage(true);
410		}));
411
412		if (this.isPagesEnabled())
413		{
414			this.keyHandler.bindAction(33, true, 'previousPage', true); // Ctrl+Shift+PageUp
415			this.keyHandler.bindAction(34, true, 'nextPage', true); // Ctrl+Shift+PageDown
416		}
417
418		// Updates the tabs after loading the diagram
419		var graph = this.editor.graph;
420		var graphViewValidateBackground = graph.view.validateBackground;
421
422		graph.view.validateBackground = mxUtils.bind(this, function()
423		{
424			if (this.tabContainer != null)
425			{
426				var prevHeight = this.tabContainer.style.height;
427
428				if (this.fileNode == null || this.pages == null ||
429					(this.pages.length == 1 && urlParams['pages'] == '0'))
430				{
431					this.tabContainer.style.height = '0px';
432				}
433				else
434				{
435					this.tabContainer.style.height = this.tabContainerHeight + 'px';
436				}
437
438				if (prevHeight != this.tabContainer.style.height)
439				{
440					this.refresh(false);
441				}
442			}
443
444			graphViewValidateBackground.apply(graph.view, arguments);
445		});
446
447		var lastPage = null;
448
449		var updateTabs = mxUtils.bind(this, function()
450		{
451			this.updateTabContainer();
452
453			// Updates scrollbar positions and backgrounds after validation
454			var p = this.currentPage;
455
456			if (p != null && p != lastPage)
457			{
458				if (p.viewState == null || p.viewState.scrollLeft == null)
459				{
460					this.resetScrollbars();
461
462					if (graph.isLightboxView())
463					{
464						this.lightboxFit();
465					}
466
467					if (this.chromelessResize != null)
468					{
469						graph.container.scrollLeft = 0;
470						graph.container.scrollTop = 0;
471						this.chromelessResize();
472					}
473				}
474				else
475				{
476					graph.container.scrollLeft = graph.view.translate.x * graph.view.scale + p.viewState.scrollLeft;
477					graph.container.scrollTop = graph.view.translate.y * graph.view.scale + p.viewState.scrollTop;
478				}
479
480				lastPage = p;
481			}
482
483			// Updates layers window
484			if (this.actions.layersWindow != null)
485			{
486				this.actions.layersWindow.refreshLayers();
487			}
488
489			// Workaround for math if tab is switched before typesetting has stopped
490			if (typeof(MathJax) !== 'undefined' && typeof(MathJax.Hub) !== 'undefined')
491			{
492				// Pending math should not be rendered if the graph has no math enabled
493				if (MathJax.Hub.queue.pending == 1 && this.editor != null && !this.editor.graph.mathEnabled)
494				{
495					// Since there is no way to stop/undo mathjax or
496					// clear the queue we have to refresh after typeset
497					MathJax.Hub.Queue(mxUtils.bind(this, function()
498					{
499						if (this.editor != null)
500						{
501							this.editor.graph.refresh();
502						}
503					}));
504				}
505			}
506			else if (typeof(Editor.MathJaxClear) !== 'undefined' && (this.editor == null || !this.editor.graph.mathEnabled))
507			{
508				// Clears our own queue for async loading
509				Editor.MathJaxClear();
510			}
511		});
512
513		// Adds a graph model listener to update the view
514		this.editor.graph.model.addListener(mxEvent.CHANGE, mxUtils.bind(this, function(sender, evt)
515		{
516			var edit = evt.getProperty('edit');
517			var changes = edit.changes;
518
519			for (var i = 0; i < changes.length; i++)
520			{
521				if (changes[i] instanceof SelectPage ||
522					changes[i] instanceof RenamePage ||
523					changes[i] instanceof MovePage ||
524					changes[i] instanceof mxRootChange)
525				{
526					updateTabs();
527					break;
528				}
529			}
530		}));
531
532		// Updates zoom in toolbar
533		if (this.toolbar != null)
534		{
535			this.editor.addListener('pageSelected', this.toolbar.updateZoom);
536		}
537	}
538};
539
540/**
541 * Adds the listener for automatically saving the diagram for local changes.
542 */
543EditorUi.prototype.restoreViewState = function(page, viewState, selection)
544{
545	var newPage = (page != null) ? this.getPageById(page.getId()) : null;
546	var graph = this.editor.graph;
547
548	if (newPage != null && this.currentPage != null && this.pages != null)
549	{
550		if (newPage != this.currentPage)
551		{
552			this.selectPage(newPage, true, viewState);
553		}
554		else
555		{
556			// TODO: Pass viewState to setGraphXml
557			graph.setViewState(viewState);
558			this.editor.updateGraphComponents();
559			graph.view.revalidate();
560			graph.sizeDidChange();
561		}
562
563		graph.container.scrollLeft = graph.view.translate.x * graph.view.scale + viewState.scrollLeft;
564		graph.container.scrollTop = graph.view.translate.y * graph.view.scale + viewState.scrollTop;
565		graph.restoreSelection(selection);
566	}
567};
568
569/**
570 * Overrides setDefaultParent
571 */
572Graph.prototype.createViewState = function(node)
573{
574	var pv = node.getAttribute('page');
575	var ps = parseFloat(node.getAttribute('pageScale'));
576	var pw = parseFloat(node.getAttribute('pageWidth'));
577	var ph = parseFloat(node.getAttribute('pageHeight'));
578	var bg = node.getAttribute('background');
579	var bgImg = this.parseBackgroundImage(node.getAttribute('backgroundImage'));
580	var extFonts = node.getAttribute('extFonts');
581
582	if (extFonts)
583	{
584		try
585		{
586			extFonts = extFonts.split('|').map(function(ef)
587			{
588				var parts = ef.split('^');
589				return {name: parts[0], url: parts[1]};
590			});
591		}
592		catch(e)
593		{
594			console.log('ExtFonts format error: ' + e.message);
595		}
596	}
597
598	return {
599		gridEnabled: node.getAttribute('grid') != '0',
600		//gridColor: node.getAttribute('gridColor') || mxSettings.getGridColor(uiTheme == 'dark'),
601		gridSize: parseFloat(node.getAttribute('gridSize')) || mxGraph.prototype.gridSize,
602		guidesEnabled: node.getAttribute('guides') != '0',
603		foldingEnabled: node.getAttribute('fold') != '0',
604		shadowVisible: node.getAttribute('shadow') == '1',
605		pageVisible: (this.isLightboxView()) ? false : ((pv != null) ? (pv != '0') : this.defaultPageVisible),
606		background: (bg != null && bg.length > 0) ? bg : null,
607		backgroundImage: bgImg,
608		pageScale: (!isNaN(ps)) ? ps : mxGraph.prototype.pageScale,
609		pageFormat: (!isNaN(pw) && !isNaN(ph)) ? new mxRectangle(0, 0, pw, ph) :
610			((typeof mxSettings === 'undefined' || this.defaultPageFormat != null) ?
611				mxGraph.prototype.pageFormat : mxSettings.getPageFormat()),
612		tooltips: node.getAttribute('tooltips') != '0',
613		connect: node.getAttribute('connect') != '0',
614		arrows: node.getAttribute('arrows') != '0',
615		mathEnabled: node.getAttribute('math') == '1',
616		selectionCells: null,
617		defaultParent: null,
618		scrollbars: this.defaultScrollbars,
619		scale: 1,
620		hiddenTags: [],
621		extFonts: extFonts || []
622	};
623};
624
625/**
626 * Writes the graph properties from the realtime model to the given mxGraphModel node.
627 */
628Graph.prototype.saveViewState = function(vs, node, ignoreTransient, resolveReferences)
629{
630	if (!ignoreTransient)
631	{
632		node.setAttribute('grid', (vs == null || vs.gridEnabled) ? '1' : '0');
633		node.setAttribute('gridSize', (vs != null) ? vs.gridSize : mxGraph.prototype.gridSize);
634		node.setAttribute('guides', (vs == null || vs.guidesEnabled) ? '1' : '0');
635		node.setAttribute('tooltips', (vs == null || vs.tooltips) ? '1' : '0');
636		node.setAttribute('connect', (vs == null || vs.connect) ? '1' : '0');
637		node.setAttribute('arrows', (vs == null || vs.arrows) ? '1' : '0');
638		node.setAttribute('page', ((vs == null && this.defaultPageVisible ) ||
639			(vs != null && vs.pageVisible)) ? '1' : '0');
640
641		// Ignores fold to avoid checksum errors for lightbox mode
642		node.setAttribute('fold', (vs == null || vs.foldingEnabled) ? '1' : '0');
643	}
644
645	node.setAttribute('pageScale', (vs != null && vs.pageScale != null) ?
646		vs.pageScale : mxGraph.prototype.pageScale);
647
648	var pf = (vs != null) ? vs.pageFormat : (typeof mxSettings === 'undefined' ||
649		this.defaultPageFormat != null) ? mxGraph.prototype.pageFormat :
650			mxSettings.getPageFormat();
651
652	if (pf != null)
653	{
654		node.setAttribute('pageWidth', pf.width);
655		node.setAttribute('pageHeight', pf.height);
656	}
657
658	if (vs != null)
659	{
660		if (vs.background != null)
661		{
662			node.setAttribute('background', vs.background);
663		}
664
665		var bgImg = this.getBackgroundImageObject(vs.backgroundImage, resolveReferences);
666
667		if (bgImg != null)
668		{
669			node.setAttribute('backgroundImage', JSON.stringify(bgImg));
670		}
671	}
672
673	node.setAttribute('math', (vs != null && vs.mathEnabled) ? '1' : '0');
674	node.setAttribute('shadow', (vs != null && vs.shadowVisible) ? '1' : '0');
675
676	if (vs != null && vs.extFonts != null && vs.extFonts.length > 0)
677	{
678		node.setAttribute('extFonts', vs.extFonts.map(function(ef)
679		{
680			return ef.name + '^' + ef.url;
681		}).join('|'));
682	}
683};
684
685/**
686 * Overrides setDefaultParent
687 */
688Graph.prototype.getViewState = function()
689{
690	return {
691		defaultParent: this.defaultParent,
692		currentRoot: this.view.currentRoot,
693		gridEnabled: this.gridEnabled,
694		//gridColor: this.view.gridColor,
695		gridSize: this.gridSize,
696		guidesEnabled: this.graphHandler.guidesEnabled,
697		foldingEnabled: this.foldingEnabled,
698		shadowVisible: this.shadowVisible,
699		scrollbars: this.scrollbars,
700		pageVisible: this.pageVisible,
701		background: this.background,
702		backgroundImage: this.backgroundImage,
703		pageScale: this.pageScale,
704		pageFormat: this.pageFormat,
705		tooltips: this.tooltipHandler.isEnabled(),
706		connect: this.connectionHandler.isEnabled(),
707		arrows: this.connectionArrowsEnabled,
708		scale: this.view.scale,
709		scrollLeft: this.container.scrollLeft - this.view.translate.x * this.view.scale,
710		scrollTop: this.container.scrollTop - this.view.translate.y * this.view.scale,
711		translate: this.view.translate.clone(),
712		lastPasteXml: this.lastPasteXml,
713		pasteCounter: this.pasteCounter,
714		mathEnabled: this.mathEnabled,
715		hiddenTags: this.hiddenTags,
716		extFonts: this.extFonts
717	};
718};
719
720/**
721 * Overrides setDefaultParent
722 */
723Graph.prototype.setViewState = function(state, removeOldExtFonts)
724{
725	if (state != null)
726	{
727		this.lastPasteXml = state.lastPasteXml;
728		this.pasteCounter = state.pasteCounter || 0;
729		this.mathEnabled = state.mathEnabled;
730		this.gridEnabled = state.gridEnabled;
731		//this.view.gridColor = state.gridColor;
732		this.gridSize = state.gridSize;
733		this.graphHandler.guidesEnabled = state.guidesEnabled;
734		this.foldingEnabled = state.foldingEnabled;
735		this.setShadowVisible(state.shadowVisible, false);
736		this.scrollbars = state.scrollbars;
737		this.pageVisible = !this.isViewer() && state.pageVisible;
738		this.background = state.background;
739		this.pageScale = state.pageScale;
740		this.pageFormat = state.pageFormat;
741		this.view.currentRoot = state.currentRoot;
742		this.defaultParent = state.defaultParent;
743		this.connectionArrowsEnabled = state.arrows;
744		this.setTooltips(state.tooltips);
745		this.setConnectable(state.connect);
746		this.setBackgroundImage(state.backgroundImage);
747		this.hiddenTags = state.hiddenTags;
748
749		var oldExtFonts = this.extFonts;
750		this.extFonts = state.extFonts || [];
751
752		// Removing old fonts is important for real-time synchronization
753		// But, for page change, it results in undesirable font flicker
754		if (removeOldExtFonts && oldExtFonts != null)
755		{
756			for (var i = 0; i < oldExtFonts.length; i++)
757			{
758				var fontElem = document.getElementById('extFont_' + oldExtFonts[i].name);
759
760				if (fontElem != null)
761				{
762					fontElem.parentNode.removeChild(fontElem);
763				}
764			}
765		}
766
767		for (var i = 0; i < this.extFonts.length; i++)
768		{
769			this.addExtFont(this.extFonts[i].name, this.extFonts[i].url, true);
770		}
771
772		if (state.scale != null)
773		{
774			this.view.scale = state.scale;
775		}
776		else
777		{
778			this.view.scale = 1;
779		}
780
781		// Checks if current root or default parent have been removed
782		if (this.view.currentRoot != null &&
783			!this.model.contains(this.view.currentRoot))
784		{
785			this.view.currentRoot = null;
786		}
787
788		if (this.defaultParent != null &&
789			!this.model.contains(this.defaultParent))
790		{
791			this.setDefaultParent(null);
792			this.selectUnlockedLayer();
793		}
794
795		if (state.translate != null)
796		{
797			this.view.translate = state.translate;
798		}
799	}
800	else
801	{
802		this.view.currentRoot = null;
803		this.view.scale = 1;
804		this.gridEnabled = this.defaultGridEnabled;
805		this.gridSize = mxGraph.prototype.gridSize;
806		this.pageScale = mxGraph.prototype.pageScale;
807		this.pageFormat = (typeof mxSettings === 'undefined' || this.defaultPageFormat != null) ?
808			mxGraph.prototype.pageFormat : mxSettings.getPageFormat();
809		this.pageVisible = this.defaultPageVisible;
810		this.background = null;
811		this.backgroundImage = null;
812		this.scrollbars = this.defaultScrollbars;
813		this.graphHandler.guidesEnabled = true;
814		this.foldingEnabled = true;
815		this.setShadowVisible(false, false);
816		this.defaultParent = null;
817		this.setTooltips(true);
818		this.setConnectable(true);
819		this.lastPasteXml = null;
820		this.pasteCounter = 0;
821		this.mathEnabled = this.defaultMathEnabled;
822		this.connectionArrowsEnabled = true;
823		this.hiddenTags = [];
824		this.extFonts = [];
825	}
826
827	// Implicit settings
828	this.pageBreaksVisible = this.pageVisible;
829	this.preferPageSize = this.pageVisible;
830	this.fireEvent(new mxEventObject('viewStateChanged', 'state', state));
831};
832
833Graph.prototype.addExtFont = function(fontName, fontUrl, dontRemember)
834{
835	// KNOWN: Font not added when pasting cells with custom fonts
836	if (fontName && fontUrl)
837	{
838		if (urlParams['ext-fonts'] != '1')
839		{
840			// Adds inserted fonts to font family menu
841			Graph.recentCustomFonts[fontName.toLowerCase()] = {name: fontName, url: fontUrl};
842		}
843
844		var fontId = 'extFont_' + fontName;
845
846		if (document.getElementById(fontId) == null)
847		{
848			if (fontUrl.indexOf(Editor.GOOGLE_FONTS) == 0)
849			{
850				mxClient.link('stylesheet', fontUrl, null, fontId);
851			}
852			else
853			{
854				var head = document.getElementsByTagName('head')[0];
855
856				// KNOWN: Should load fonts synchronously
857				var style = document.createElement('style');
858
859				style.appendChild(document.createTextNode('@font-face {\n' +
860					'\tfont-family: "'+ fontName +'";\n' +
861					'\tsrc: url("'+ fontUrl +'");\n}'));
862
863				style.setAttribute('id', fontId);
864				var head = document.getElementsByTagName('head')[0];
865		   		head.appendChild(style);
866			}
867		}
868
869		if (!dontRemember)
870		{
871			if (this.extFonts == null)
872			{
873				this.extFonts = [];
874			}
875
876			var extFonts = this.extFonts, notFound = true;
877
878			for (var i = 0; i < extFonts.length; i++)
879			{
880				if (extFonts[i].name == fontName)
881				{
882					notFound = false;
883					break;
884				}
885			}
886
887			if (notFound)
888			{
889				this.extFonts.push({name: fontName, url: fontUrl});
890			}
891		}
892	}
893};
894
895/**
896 * Executes selection of a new page.
897 */
898EditorUi.prototype.updatePageRoot = function(page, checked)
899{
900	if (page.root == null)
901	{
902		var node = this.editor.extractGraphModel(page.node, null, checked);
903		var cause = Editor.extractParserError(node);
904
905		if (cause)
906		{
907			throw new Error(cause);
908		}
909		else if (node != null)
910		{
911			page.graphModelNode = node;
912
913			// Converts model XML into page object with root cell
914			page.viewState = this.editor.graph.createViewState(node);
915			var codec = new mxCodec(node.ownerDocument);
916			page.root = codec.decode(node).root;
917		}
918		else
919		{
920			// Initializes page object with new empty root
921			page.root = this.editor.graph.model.createRoot();
922		}
923	}
924	else if (page.viewState == null)
925	{
926		if (page.graphModelNode == null)
927		{
928			var node = this.editor.extractGraphModel(page.node);
929
930			var cause = Editor.extractParserError(node);
931
932			if (cause)
933			{
934				throw new Error(cause);
935			}
936			else if (node != null)
937			{
938				page.graphModelNode = node;
939			}
940		}
941
942		if (page.graphModelNode != null)
943		{
944			page.viewState = this.editor.graph.createViewState(page.graphModelNode);
945		}
946	}
947
948	return page;
949};
950
951/**
952 * Returns true if the given string contains an mxfile.
953 */
954EditorUi.prototype.selectPage = function(page, quiet, viewState)
955{
956	try
957	{
958		if (page != this.currentPage)
959		{
960			if (this.editor.graph.isEditing())
961			{
962				this.editor.graph.stopEditing(false);
963			}
964
965			quiet = (quiet != null) ? quiet : false;
966			this.editor.graph.isMouseDown = false;
967			this.editor.graph.reset();
968
969			var edit = this.editor.graph.model.createUndoableEdit();
970
971			// Special flag to bypass autosave for this edit
972			edit.ignoreEdit = true;
973
974			var change = new SelectPage(this, page, viewState);
975			change.execute();
976			edit.add(change);
977			edit.notify();
978
979			this.editor.graph.tooltipHandler.hide();
980
981			if (!quiet)
982			{
983				this.editor.graph.model.fireEvent(new mxEventObject(mxEvent.UNDO, 'edit', edit));
984			}
985		}
986	}
987	catch (e)
988	{
989		this.handleError(e);
990	}
991};
992
993/**
994 *
995 */
996EditorUi.prototype.selectNextPage = function(forward)
997{
998	var next = this.currentPage;
999
1000	if (next != null && this.pages != null)
1001	{
1002		var tmp = mxUtils.indexOf(this.pages, next);
1003
1004		if (forward)
1005		{
1006			this.selectPage(this.pages[mxUtils.mod(tmp + 1, this.pages.length)]);
1007		}
1008		else if (!forward)
1009		{
1010			this.selectPage(this.pages[mxUtils.mod(tmp - 1, this.pages.length)]);
1011		}
1012	}
1013};
1014
1015/**
1016 * Returns true if the given string contains an mxfile.
1017 */
1018EditorUi.prototype.insertPage = function(page, index)
1019{
1020	if (this.editor.graph.isEnabled())
1021	{
1022		if (this.editor.graph.isEditing())
1023		{
1024			this.editor.graph.stopEditing(false);
1025		}
1026
1027		page = (page != null) ? page : this.createPage(null, this.createPageId());
1028		index = (index != null) ? index : this.pages.length;
1029
1030		// Uses model to fire event and trigger autosave
1031		var change = new ChangePage(this, page, page, index);
1032		this.editor.graph.model.execute(change);
1033	}
1034
1035	return page;
1036};
1037
1038/**
1039 * Returns a unique page ID.
1040 */
1041EditorUi.prototype.createPageId = function()
1042{
1043	var id = null;
1044
1045	do
1046	{
1047		id = Editor.guid();
1048	} while (this.getPageById(id) != null)
1049
1050	return id;
1051};
1052
1053/**
1054 * Returns a new DiagramPage instance.
1055 */
1056EditorUi.prototype.createPage = function(name, id)
1057{
1058	var page = new DiagramPage(this.fileNode.ownerDocument.createElement('diagram'), id);
1059	page.setName((name != null) ? name : this.createPageName());
1060
1061	return page;
1062};
1063
1064/**
1065 * Returns a page name.
1066 */
1067EditorUi.prototype.createPageName = function()
1068{
1069	// Creates a lookup with names
1070	var existing = {};
1071
1072	for (var i = 0; i < this.pages.length; i++)
1073	{
1074		var tmp = this.pages[i].getName();
1075
1076		if (tmp != null && tmp.length > 0)
1077		{
1078			existing[tmp] = tmp;
1079		}
1080	}
1081
1082	// Avoids existing names
1083	var nr = this.pages.length;
1084	var name = null;
1085
1086	do
1087	{
1088		name = mxResources.get('pageWithNumber', [++nr]);
1089	}
1090	while (existing[name] != null);
1091
1092	return name;
1093};
1094
1095/**
1096 * Removes the given page.
1097 */
1098EditorUi.prototype.removePage = function(page)
1099{
1100	try
1101	{
1102		var graph = this.editor.graph;
1103		var tmp = mxUtils.indexOf(this.pages, page);
1104
1105		if (graph.isEnabled() && tmp >= 0)
1106		{
1107			if (this.editor.graph.isEditing())
1108			{
1109				this.editor.graph.stopEditing(false);
1110			}
1111
1112			graph.model.beginUpdate();
1113			try
1114			{
1115				var next = this.currentPage;
1116
1117				if (next == page && this.pages.length > 1)
1118				{
1119					if (tmp == this.pages.length - 1)
1120					{
1121						tmp--;
1122					}
1123					else
1124					{
1125						tmp++;
1126					}
1127
1128					next = this.pages[tmp];
1129				}
1130				else if (this.pages.length <= 1)
1131				{
1132					// Removes label with incorrect page number to force
1133					// default page name which is OK for a single page
1134					next = this.insertPage();
1135					graph.model.execute(new RenamePage(this, next,
1136						mxResources.get('pageWithNumber', [1])));
1137				}
1138
1139				// Uses model to fire event to trigger autosave
1140				graph.model.execute(new ChangePage(this, page, next));
1141			}
1142			finally
1143			{
1144				graph.model.endUpdate();
1145			}
1146		}
1147	}
1148	catch (e)
1149	{
1150		this.handleError(e);
1151	}
1152
1153	return page;
1154};
1155
1156/**
1157 * Duplicates the given page.
1158 */
1159EditorUi.prototype.duplicatePage = function(page, name)
1160{
1161	var newPage = null;
1162
1163	try
1164	{
1165		var graph = this.editor.graph;
1166
1167		if (graph.isEnabled())
1168		{
1169			if (graph.isEditing())
1170			{
1171				graph.stopEditing();
1172			}
1173
1174			// Clones the current page and takes a snapshot of the graph model and view state
1175			var node = page.node.cloneNode(false);
1176			node.removeAttribute('id');
1177
1178			var cloneMap = new Object();
1179			var lookup = graph.createCellLookup([graph.model.root]);
1180
1181			var newPage = new DiagramPage(node);
1182			newPage.root = graph.cloneCell(graph.model.root, null, cloneMap);
1183			newPage.viewState = graph.getViewState();
1184
1185			// Resets zoom and scrollbar positions
1186			newPage.viewState.scale = 1;
1187			newPage.viewState.scrollLeft = null;
1188			newPage.viewState.scrollTop = null;
1189			newPage.viewState.currentRoot = null;
1190			newPage.viewState.defaultParent = null;
1191			newPage.setName(name);
1192
1193			newPage = this.insertPage(newPage, mxUtils.indexOf(this.pages, page) + 1);
1194
1195			// Updates custom links after inserting into the model for cells to have new IDs
1196			graph.updateCustomLinks(graph.createCellMapping(cloneMap, lookup), [newPage.root]);
1197		}
1198	}
1199	catch (e)
1200	{
1201		this.handleError(e);
1202	}
1203
1204	return newPage;
1205};
1206
1207/**
1208 * Renames the given page using a dialog.
1209 */
1210EditorUi.prototype.renamePage = function(page)
1211{
1212	var graph = this.editor.graph;
1213
1214	if (graph.isEnabled())
1215	{
1216		var dlg = new FilenameDialog(this, page.getName(), mxResources.get('rename'), mxUtils.bind(this, function(name)
1217		{
1218			if (name != null && name.length > 0)
1219			{
1220				this.editor.graph.model.execute(new RenamePage(this, page, name));
1221			}
1222		}), mxResources.get('rename'));
1223		this.showDialog(dlg.container, 300, 80, true, true);
1224		dlg.init();
1225	}
1226
1227	return page;
1228}
1229
1230/**
1231 * Returns true if the given string contains an mxfile.
1232 */
1233EditorUi.prototype.movePage = function(oldIndex, newIndex)
1234{
1235	this.editor.graph.model.execute(new MovePage(this, oldIndex, newIndex));
1236}
1237
1238/**
1239 * Returns true if the given string contains an mxfile.
1240 */
1241EditorUi.prototype.createTabContainer = function()
1242{
1243	var div = document.createElement('div');
1244	div.className = 'geTabContainer';
1245	div.style.position = 'absolute';
1246	div.style.whiteSpace = 'nowrap';
1247	div.style.overflow = 'hidden';
1248	div.style.height = '0px';
1249
1250	return div;
1251};
1252
1253/**
1254 * Returns true if the given string contains an mxfile.
1255 */
1256EditorUi.prototype.updateTabContainer = function()
1257{
1258	if (this.tabContainer != null && this.pages != null)
1259	{
1260		var graph = this.editor.graph;
1261		var wrapper = document.createElement('div');
1262		wrapper.style.position = 'relative';
1263		wrapper.style.display = 'inline-block';
1264		wrapper.style.verticalAlign = 'top';
1265		wrapper.style.height = this.tabContainer.style.height;
1266		wrapper.style.whiteSpace = 'nowrap';
1267		wrapper.style.overflow = 'hidden';
1268		wrapper.style.fontSize = '13px';
1269
1270		// Allows for negative left margin of first tab
1271		wrapper.style.marginLeft = '30px';
1272
1273		// Automatic tab width to match available width
1274		// TODO: Fix tabWidth in chromeless mode
1275		var btnWidth = (this.editor.isChromelessView()) ? 29 : 59;
1276		var tabWidth = Math.min(140, Math.max(20, (this.tabContainer.clientWidth - btnWidth) / this.pages.length) + 1);
1277		var startIndex = null;
1278
1279		for (var i = 0; i < this.pages.length; i++)
1280		{
1281			// Install drag and drop for page reorder
1282			(mxUtils.bind(this, function(index, tab)
1283			{
1284				if (this.pages[index] == this.currentPage)
1285				{
1286					tab.className = 'geActivePage';
1287					tab.style.backgroundColor = Editor.isDarkMode() ? Editor.darkColor : '#fff';
1288				}
1289				else
1290				{
1291					tab.className = 'geInactivePage';
1292				}
1293
1294				tab.setAttribute('draggable', 'true');
1295
1296				mxEvent.addListener(tab, 'dragstart', mxUtils.bind(this, function(evt)
1297				{
1298					if (graph.isEnabled())
1299					{
1300						// Workaround for no DnD on DIV in FF
1301						if (mxClient.IS_FF)
1302						{
1303							// LATER: Check what triggers a parse as XML on this in FF after drop
1304							evt.dataTransfer.setData('Text', '<diagram/>');
1305						}
1306
1307						startIndex = index;
1308					}
1309					else
1310					{
1311						// Blocks event
1312						mxEvent.consume(evt);
1313					}
1314				}));
1315
1316				mxEvent.addListener(tab, 'dragend', mxUtils.bind(this, function(evt)
1317				{
1318					startIndex = null;
1319					evt.stopPropagation();
1320					evt.preventDefault();
1321				}));
1322
1323				mxEvent.addListener(tab, 'dragover', mxUtils.bind(this, function(evt)
1324				{
1325					if (startIndex != null)
1326					{
1327						evt.dataTransfer.dropEffect = 'move';
1328					}
1329
1330					evt.stopPropagation();
1331					evt.preventDefault();
1332				}));
1333
1334				mxEvent.addListener(tab, 'drop', mxUtils.bind(this, function(evt)
1335				{
1336					if (startIndex != null && index != startIndex)
1337					{
1338						// LATER: Shift+drag for merge, ctrl+drag for clone
1339						this.movePage(startIndex, index);
1340					}
1341
1342					evt.stopPropagation();
1343					evt.preventDefault();
1344				}));
1345
1346				wrapper.appendChild(tab);
1347			}))(i, this.createTabForPage(this.pages[i], tabWidth, this.pages[i] != this.currentPage, i + 1));
1348		}
1349
1350		this.tabContainer.innerHTML = '';
1351		this.tabContainer.appendChild(wrapper);
1352
1353		// Adds floating menu with all pages and insert option
1354		var menutab = this.createPageMenuTab();
1355		this.tabContainer.appendChild(menutab);
1356		var insertTab = null;
1357
1358		// Not chromeless and not read-only file
1359		if (this.isPageInsertTabVisible())
1360		{
1361			insertTab = this.createPageInsertTab();
1362			this.tabContainer.appendChild(insertTab);
1363		}
1364
1365		if (wrapper.clientWidth > this.tabContainer.clientWidth - btnWidth)
1366		{
1367			if (insertTab != null)
1368			{
1369				insertTab.style.position = 'absolute';
1370				insertTab.style.right = '0px';
1371				wrapper.style.marginRight = '30px';
1372			}
1373
1374			var temp = this.createControlTab(4, '&nbsp;&#10094;&nbsp;');
1375			temp.style.position = 'absolute';
1376			temp.style.right = (this.editor.chromeless) ? '29px' : '55px';
1377			temp.style.fontSize = '13pt';
1378
1379			this.tabContainer.appendChild(temp);
1380
1381			var temp2 = this.createControlTab(4, '&nbsp;&#10095;');
1382			temp2.style.position = 'absolute';
1383			temp2.style.right = (this.editor.chromeless) ? '0px' : '29px';
1384			temp2.style.fontSize = '13pt';
1385
1386			this.tabContainer.appendChild(temp2);
1387
1388			// TODO: Scroll to current page
1389			var dx = Math.max(0, this.tabContainer.clientWidth - ((this.editor.chromeless) ? 86 : 116));
1390			wrapper.style.width = dx + 'px';
1391
1392			var fade = 50;
1393
1394			mxEvent.addListener(temp, 'click', mxUtils.bind(this, function(evt)
1395			{
1396				wrapper.scrollLeft -= Math.max(20, dx - 20);
1397				mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade);
1398				mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade);
1399				mxEvent.consume(evt);
1400			}));
1401
1402			mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade);
1403			mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade);
1404
1405			mxEvent.addListener(temp2, 'click', mxUtils.bind(this, function(evt)
1406			{
1407				wrapper.scrollLeft += Math.max(20, dx - 20);
1408				mxUtils.setOpacity(temp, (wrapper.scrollLeft > 0) ? 100 : fade);
1409				mxUtils.setOpacity(temp2, (wrapper.scrollLeft < wrapper.scrollWidth - wrapper.clientWidth) ? 100 : fade);
1410				mxEvent.consume(evt);
1411			}));
1412		}
1413	}
1414};
1415
1416/**
1417 * Returns true if the given string contains an mxfile.
1418 */
1419EditorUi.prototype.isPageInsertTabVisible = function()
1420{
1421	return urlParams['embed'] == 1 || (this.getCurrentFile() != null &&
1422		this.getCurrentFile().isEditable());
1423};
1424
1425/**
1426 * Returns true if the given string contains an mxfile.
1427 */
1428EditorUi.prototype.createTab = function(hoverEnabled)
1429{
1430	var tab = document.createElement('div');
1431	tab.style.display = 'inline-block';
1432	tab.style.whiteSpace = 'nowrap';
1433	tab.style.boxSizing = 'border-box';
1434	tab.style.position = 'relative';
1435	tab.style.overflow = 'hidden';
1436	tab.style.textAlign = 'center';
1437	tab.style.marginLeft = '-1px';
1438	tab.style.height = this.tabContainer.clientHeight + 'px';
1439	tab.style.padding = '12px 4px 8px 4px';
1440	tab.style.border = Editor.isDarkMode() ? '1px solid #505759' : '1px solid #e8eaed';
1441	tab.style.borderTopStyle = 'none';
1442	tab.style.borderBottomStyle = 'none';
1443	tab.style.backgroundColor = this.tabContainer.style.backgroundColor;
1444	tab.style.cursor = 'move';
1445	tab.style.color = 'gray';
1446
1447	if (hoverEnabled)
1448	{
1449		mxEvent.addListener(tab, 'mouseenter', mxUtils.bind(this, function(evt)
1450		{
1451			if (!this.editor.graph.isMouseDown)
1452			{
1453				tab.style.backgroundColor = Editor.isDarkMode() ? 'black' : '#e8eaed';
1454				mxEvent.consume(evt);
1455			}
1456		}));
1457
1458		mxEvent.addListener(tab, 'mouseleave', mxUtils.bind(this, function(evt)
1459		{
1460			tab.style.backgroundColor = this.tabContainer.style.backgroundColor;
1461			mxEvent.consume(evt);
1462		}));
1463	}
1464
1465	return tab;
1466};
1467
1468/**
1469 * Returns true if the given string contains an mxfile.
1470 */
1471EditorUi.prototype.createControlTab = function(paddingTop, html, hoverEnabled)
1472{
1473	var tab = this.createTab((hoverEnabled != null) ? hoverEnabled : true);
1474	tab.style.lineHeight = this.tabContainerHeight + 'px';
1475	tab.style.paddingTop = paddingTop + 'px';
1476	tab.style.cursor = 'pointer';
1477	tab.style.width = '30px';
1478	tab.innerHTML = html;
1479
1480	if (tab.firstChild != null && tab.firstChild.style != null)
1481	{
1482		mxUtils.setOpacity(tab.firstChild, 40);
1483	}
1484
1485	return tab;
1486};
1487
1488/**
1489 * Returns true if the given string contains an mxfile.
1490 */
1491EditorUi.prototype.createPageMenuTab = function(hoverEnabled)
1492{
1493	var tab = this.createControlTab(3, '<div class="geSprite geSprite-dots"></div>', hoverEnabled);
1494	tab.setAttribute('title', mxResources.get('pages'));
1495	tab.style.position = 'absolute';
1496	tab.style.marginLeft = '0px';
1497	tab.style.top = '0px';
1498	tab.style.left = '1px';
1499
1500	var div = tab.getElementsByTagName('div')[0];
1501	div.style.display = 'inline-block';
1502	div.style.marginTop = '5px';
1503	div.style.width = '21px';
1504	div.style.height = '21px';
1505
1506	mxEvent.addListener(tab, 'click', mxUtils.bind(this, function(evt)
1507	{
1508		this.editor.graph.popupMenuHandler.hideMenu();
1509		var menu = new mxPopupMenu(mxUtils.bind(this, function(menu, parent)
1510		{
1511			for (var i = 0; i < this.pages.length; i++)
1512			{
1513				(mxUtils.bind(this, function(index)
1514				{
1515					var item = menu.addItem(this.pages[index].getName(), null, mxUtils.bind(this, function()
1516					{
1517						this.selectPage(this.pages[index]);
1518					}), parent);
1519
1520					var id = this.pages[index].getId();
1521					item.setAttribute('title', this.pages[index].getName() +
1522						((id != null) ? ' (' + id + ')' : '') +
1523						' [' + (index + 1)+ ']');
1524
1525					// Adds checkmark to current page
1526					if (this.pages[index] == this.currentPage)
1527					{
1528						menu.addCheckmark(item, Editor.checkmarkImage);
1529					}
1530				}))(i);
1531			}
1532
1533			if (this.editor.graph.isEnabled())
1534			{
1535				menu.addSeparator(parent);
1536
1537				var item = menu.addItem(mxResources.get('insertPage'), null, mxUtils.bind(this, function()
1538				{
1539					this.insertPage();
1540				}), parent);
1541
1542				var page = this.currentPage;
1543
1544				if (page != null)
1545				{
1546					menu.addSeparator(parent);
1547					var pageName = page.getName();
1548
1549					menu.addItem(mxResources.get('removeIt', [pageName]), null, mxUtils.bind(this, function()
1550					{
1551						this.removePage(page);
1552					}), parent);
1553
1554					menu.addItem(mxResources.get('renameIt', [pageName]), null, mxUtils.bind(this, function()
1555					{
1556						this.renamePage(page, page.getName());
1557					}), parent);
1558
1559					menu.addSeparator(parent);
1560
1561					menu.addItem(mxResources.get('duplicateIt', [pageName]), null, mxUtils.bind(this, function()
1562					{
1563						this.duplicatePage(page, mxResources.get('copyOf', [page.getName()]));
1564					}), parent);
1565				}
1566			}
1567		}));
1568
1569		menu.div.className += ' geMenubarMenu';
1570		menu.smartSeparators = true;
1571		menu.showDisabled = true;
1572		menu.autoExpand = true;
1573
1574		// Disables autoexpand and destroys menu when hidden
1575		menu.hideMenu = mxUtils.bind(this, function()
1576		{
1577			mxPopupMenu.prototype.hideMenu.apply(menu, arguments);
1578			menu.destroy();
1579		});
1580
1581		var x = mxEvent.getClientX(evt);
1582		var y = mxEvent.getClientY(evt);
1583		menu.popup(x, y, null, evt);
1584
1585		// Allows hiding by clicking on document
1586		this.setCurrentMenu(menu);
1587
1588		mxEvent.consume(evt);
1589	}));
1590
1591	return tab;
1592};
1593
1594/**
1595 * Returns true if the given string contains an mxfile.
1596 */
1597EditorUi.prototype.createPageInsertTab = function()
1598{
1599	var tab = this.createControlTab(4, '<div class="geSprite geSprite-plus"></div>');
1600	tab.setAttribute('title', mxResources.get('insertPage'));
1601	var graph = this.editor.graph;
1602
1603	mxEvent.addListener(tab, 'click', mxUtils.bind(this, function(evt)
1604	{
1605		this.insertPage();
1606		mxEvent.consume(evt);
1607	}));
1608
1609	var div = tab.getElementsByTagName('div')[0];
1610	div.style.display = 'inline-block';
1611	div.style.width = '21px';
1612	div.style.height = '21px';
1613
1614	return tab;
1615};
1616
1617/**
1618 * Returns true if the given string contains an mxfile.
1619 */
1620EditorUi.prototype.createTabForPage = function(page, tabWidth, hoverEnabled, pageNumber)
1621{
1622	var tab = this.createTab(hoverEnabled);
1623	var name = page.getName() || mxResources.get('untitled');
1624	var id = page.getId();
1625	tab.setAttribute('title', name + ((id != null) ? ' (' + id + ')' : '') + ' [' + pageNumber + ']');
1626	mxUtils.write(tab, name);
1627	tab.style.maxWidth = tabWidth + 'px';
1628	tab.style.width = tabWidth + 'px';
1629	this.addTabListeners(page, tab);
1630
1631	if (tabWidth > 42)
1632	{
1633		tab.style.textOverflow = 'ellipsis';
1634	}
1635
1636	return tab;
1637};
1638
1639/**
1640 * Translates this point by the given vector.
1641 *
1642 * @param {number} dx X-coordinate of the translation.
1643 * @param {number} dy Y-coordinate of the translation.
1644 */
1645EditorUi.prototype.addTabListeners = function(page, tab)
1646{
1647	mxEvent.disableContextMenu(tab);
1648	var graph = this.editor.graph;
1649	var model = graph.model;
1650
1651	mxEvent.addListener(tab, 'dblclick', mxUtils.bind(this, function(evt)
1652	{
1653		this.renamePage(page)
1654		mxEvent.consume(evt);
1655	}));
1656
1657	var menuWasVisible = false;
1658	var pageWasActive = false;
1659
1660	mxEvent.addGestureListeners(tab, mxUtils.bind(this, function(evt)
1661	{
1662		// Do not consume event here to allow for drag and drop of tabs
1663		menuWasVisible = this.currentMenu != null;
1664		pageWasActive = page == this.currentPage;
1665
1666		if (!graph.isMouseDown && !pageWasActive)
1667		{
1668			this.selectPage(page);
1669		}
1670	}), null, mxUtils.bind(this, function(evt)
1671	{
1672		if (graph.isEnabled() && !graph.isMouseDown &&
1673			((mxEvent.isTouchEvent(evt) && pageWasActive) ||
1674			mxEvent.isPopupTrigger(evt)))
1675		{
1676			graph.popupMenuHandler.hideMenu();
1677			this.hideCurrentMenu();
1678
1679			if (!mxEvent.isTouchEvent(evt) || !menuWasVisible)
1680			{
1681				var menu = new mxPopupMenu(this.createPageMenu(page));
1682
1683				menu.div.className += ' geMenubarMenu';
1684				menu.smartSeparators = true;
1685				menu.showDisabled = true;
1686				menu.autoExpand = true;
1687
1688				// Disables autoexpand and destroys menu when hidden
1689				menu.hideMenu = mxUtils.bind(this, function()
1690				{
1691					mxPopupMenu.prototype.hideMenu.apply(menu, arguments);
1692					this.resetCurrentMenu();
1693					menu.destroy();
1694				});
1695
1696				var x = mxEvent.getClientX(evt);
1697				var y = mxEvent.getClientY(evt);
1698				menu.popup(x, y, null, evt);
1699				this.setCurrentMenu(menu, tab);
1700			}
1701
1702			mxEvent.consume(evt);
1703		}
1704	}));
1705};
1706
1707/**
1708 * Returns an absolute URL to the given page or null of absolute links
1709 * to pages are not supported in this file.
1710 */
1711EditorUi.prototype.getLinkForPage = function(page, params, lightbox)
1712{
1713	if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp)
1714	{
1715		var file = this.getCurrentFile();
1716
1717		if (file != null && file.constructor != LocalFile && this.getServiceName() == 'draw.io')
1718		{
1719			var search = this.getSearch(['create', 'title', 'mode', 'url', 'drive', 'splash',
1720				'state', 'clibs', 'ui', 'viewbox', 'hide-pages']);
1721			search += ((search.length == 0) ? '?' : '&') + 'page-id=' + page.getId();
1722
1723			if (params != null)
1724			{
1725				search += '&' + params.join('&');
1726			}
1727
1728			return ((lightbox && urlParams['dev'] != '1') ? EditorUi.lightboxHost :
1729				(((mxClient.IS_CHROMEAPP || EditorUi.isElectronApp ||
1730				!(/.*\.draw\.io$/.test(window.location.hostname))) ?
1731				EditorUi.drawHost : 'https://' + window.location.host))) +
1732				'/' + search + '#' + file.getHash();
1733		}
1734	}
1735
1736	return null;
1737};
1738
1739/**
1740 * Returns true if the given string contains an mxfile.
1741 */
1742EditorUi.prototype.createPageMenu = function(page, label)
1743{
1744	return mxUtils.bind(this, function(menu, parent)
1745	{
1746		var graph = this.editor.graph;
1747		var model = graph.model;
1748
1749		menu.addItem(mxResources.get('insert'), null, mxUtils.bind(this, function()
1750		{
1751			this.insertPage(null, mxUtils.indexOf(this.pages, page) + 1);
1752		}), parent);
1753
1754		menu.addItem(mxResources.get('delete'), null, mxUtils.bind(this, function()
1755		{
1756			this.removePage(page);
1757		}), parent);
1758
1759		menu.addItem(mxResources.get('rename'), null, mxUtils.bind(this, function()
1760		{
1761			this.renamePage(page, label);
1762		}), parent);
1763
1764		var url = this.getLinkForPage(page);
1765
1766		if (url != null)
1767		{
1768			menu.addSeparator(parent);
1769
1770			menu.addItem(mxResources.get('link'), null, mxUtils.bind(this, function()
1771			{
1772				this.showPublishLinkDialog(mxResources.get('url'), true, null, null,
1773					mxUtils.bind(this, function(linkTarget, linkColor, allPages, lightbox, editLink, layers)
1774				{
1775					var params = this.createUrlParameters(linkTarget, linkColor, allPages, lightbox, editLink, layers);
1776
1777					if (!allPages)
1778					{
1779						params.push('hide-pages=1');
1780					}
1781
1782					if (!graph.isSelectionEmpty())
1783					{
1784						var bounds = graph.getBoundingBox(graph.getSelectionCells());
1785
1786						var t = graph.view.translate;
1787						var s = graph.view.scale;
1788						bounds.width /= s;
1789						bounds.height /= s;
1790						bounds.x = bounds.x / s - t.x;
1791						bounds.y = bounds.y / s - t.y;
1792
1793						params.push('viewbox=' + encodeURIComponent(JSON.stringify({x: Math.round(bounds.x), y: Math.round(bounds.y),
1794							width: Math.round(bounds.width), height: Math.round(bounds.height), border: 100})));
1795					}
1796
1797					var dlg = new EmbedDialog(this, this.getLinkForPage(page, params, lightbox));
1798					this.showDialog(dlg.container, 450, 240, true, true);
1799					dlg.init();
1800				}));
1801			}));
1802		}
1803
1804		menu.addSeparator(parent);
1805
1806		menu.addItem(mxResources.get('duplicate'), null, mxUtils.bind(this, function()
1807		{
1808			this.duplicatePage(page, mxResources.get('copyOf', [page.getName()]));
1809		}), parent);
1810
1811		if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && this.getServiceName() == 'draw.io')
1812		{
1813			menu.addSeparator(parent);
1814
1815			menu.addItem(mxResources.get('openInNewWindow'), null, mxUtils.bind(this, function()
1816			{
1817				this.editor.editAsNew(this.getFileData(true, null, null, null, true, true));
1818			}), parent);
1819		}
1820	});
1821};
1822
1823// Overrides refresh to repaint tab container
1824(function()
1825{
1826	var editorUiRefresh = EditorUi.prototype.refresh;
1827
1828	EditorUi.prototype.refresh = function(sizeDidChange)
1829	{
1830		editorUiRefresh.apply(this, arguments);
1831		this.updateTabContainer();
1832	}
1833})();
1834
1835//Overrides ChangePageSetup codec to exclude page
1836(function()
1837{
1838	var codec = mxCodecRegistry.getCodec(ChangePageSetup);
1839	codec.exclude.push('page');
1840})();
1841
1842//Registers codec for MovePage
1843(function()
1844{
1845	var codec = new mxObjectCodec(new MovePage(), ['ui']);
1846
1847	codec.beforeDecode = function(dec, node, obj)
1848	{
1849		obj.ui = dec.ui;
1850
1851		return node;
1852	};
1853
1854	codec.afterDecode = function(dec, node, obj)
1855	{
1856		var tmp = obj.oldIndex;
1857		obj.oldIndex = obj.newIndex;
1858		obj.newIndex = tmp;
1859
1860	    return obj;
1861	};
1862
1863	mxCodecRegistry.register(codec);
1864})();
1865
1866//Registers codec for RenamePage
1867(function()
1868{
1869	var codec = new mxObjectCodec(new RenamePage(), ['ui', 'page']);
1870
1871	codec.beforeDecode = function(dec, node, obj)
1872	{
1873		obj.ui = dec.ui;
1874
1875		return node;
1876	};
1877
1878	codec.afterDecode = function(dec, node, obj)
1879	{
1880	    var tmp = obj.previous;
1881	    obj.previous = obj.name;
1882	    obj.name = tmp;
1883
1884	    return obj;
1885	};
1886
1887	mxCodecRegistry.register(codec);
1888})();
1889
1890//Registers codec for ChangePage
1891(function()
1892{
1893	var codec = new mxObjectCodec(new ChangePage(), ['ui', 'relatedPage',
1894		'index', 'neverShown', 'page', 'previousPage']);
1895
1896	var viewStateIgnored = ['defaultParent', 'currentRoot', 'scrollLeft',
1897		'scrollTop', 'scale', 'translate', 'lastPasteXml', 'pasteCounter'];
1898
1899	codec.afterEncode = function(enc, obj, node)
1900	{
1901		node.setAttribute('relatedPage', obj.relatedPage.getId())
1902
1903		if (obj.index == null)
1904		{
1905			node.setAttribute('name', obj.relatedPage.getName());
1906
1907			if (obj.relatedPage.viewState != null)
1908			{
1909	        	node.setAttribute('viewState', JSON.stringify(
1910	        		obj.relatedPage.viewState, function(key, value)
1911	        	{
1912	        		return (mxUtils.indexOf(viewStateIgnored, key) < 0) ? value : undefined;
1913	        	}));
1914			}
1915
1916			if (obj.relatedPage.root != null)
1917			{
1918				enc.encodeCell(obj.relatedPage.root, node);
1919			}
1920	    }
1921
1922	    return node;
1923	};
1924
1925	codec.beforeDecode = function(dec, node, obj)
1926	{
1927		obj.ui = dec.ui;
1928		obj.relatedPage = obj.ui.getPageById(node.getAttribute('relatedPage'));
1929
1930		if (obj.relatedPage == null)
1931		{
1932			var temp = node.ownerDocument.createElement('diagram');
1933			temp.setAttribute('id', node.getAttribute('relatedPage'));
1934			temp.setAttribute('name', node.getAttribute('name'));
1935			obj.relatedPage = new DiagramPage(temp);
1936
1937			var vs = node.getAttribute('viewState');
1938
1939			if (vs != null)
1940			{
1941				obj.relatedPage.viewState = JSON.parse(vs);
1942				node.removeAttribute('viewState');
1943			}
1944
1945	        // Makes sure the original node isn't modified
1946			node = node.cloneNode(true);
1947			var tmp = node.firstChild;
1948
1949			if (tmp != null)
1950			{
1951				obj.relatedPage.root = dec.decodeCell(tmp, false);
1952
1953				var tmp2 = tmp.nextSibling;
1954				tmp.parentNode.removeChild(tmp);
1955				tmp = tmp2;
1956
1957				while (tmp != null)
1958				{
1959					tmp2 = tmp.nextSibling;
1960
1961					if (tmp.nodeType == mxConstants.NODETYPE_ELEMENT)
1962					{
1963						// Ignores all existing cells because those do not need to
1964						// be re-inserted into the model. Since the encoded version
1965						// of these cells contains the new parent, this would leave
1966						// to an inconsistent state on the model (ie. a parent
1967						// change without a call to parentForCellChanged).
1968						var id = tmp.getAttribute('id');
1969
1970						if (dec.lookup(id) == null)
1971						{
1972							dec.decodeCell(tmp);
1973						}
1974					}
1975
1976					tmp.parentNode.removeChild(tmp);
1977					tmp = tmp2;
1978				}
1979			}
1980		}
1981
1982		return node;
1983	};
1984
1985	codec.afterDecode = function(dec, node, obj)
1986	{
1987		obj.index = obj.previousIndex;
1988
1989		return obj;
1990	};
1991
1992	mxCodecRegistry.register(codec);
1993})();