/**
 * Copyright (c) 2006-2026, JGraph Holdings Ltd
 * Copyright (c) 2006-2026, draw.io AG
 */
(function()
{
	if (typeof html4 !== 'undefined')
	{
		/**
		 * Enables paste from Lucidchart
		 */
		html4.ATTRIBS['span::data-lucid-content'] = 0;
		html4.ATTRIBS['span::data-lucid-type'] = 0;
		
		/**
		 * Enables custom fonts in labels.
		 */
		html4.ATTRIBS['font::data-font-src'] = 0;
	}
	
	/**
	 * Definitions for sketch font styles.
	 */
	Editor.sketchFontFamily = 'Architects Daughter';
	Editor.sketchFontSource = 'https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter';
	Editor.sketchFonts = [{'fontFamily': Editor.sketchFontFamily, 'fontUrl': decodeURIComponent(Editor.sketchFontSource)}];
	Editor.sketchDefaultCurveFitting = '1';
	Editor.sketchDefaultJiggle = '2';
		
	/**
	 * Icons for new UI style exported from https://fonts.google.com/icons (FFill: 0 Weight: 300 Grade: 0 Optical size: 48).
	 */
	Editor.thinFormatImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0ibTM2LjUgMTguMzUtNi44NS02Ljg1IDMuMjUtMy4ycS44NS0uODUgMi4wMjUtLjg1IDEuMTc1IDAgMi4wMjUuODVsMi43NSAyLjhxLjg1LjguODUgMnQtLjg1IDJaTTYuODUgNDEuMXYtNi45bDkuNTUtOS41NUw1LjUgMTMuN2w3LjgtNy44NSAxMSAxMSA1LjM1LTUuMzUgNi44NSA2Ljg1LTUuMzUgNS4zNSAxMSAxMS03Ljg1IDcuNzUtMTAuOTUtMTAuOS05LjYgOS41NVptMTEuMi0xOC4wNSA0LjY1LTQuNi00LjA1LTQuMDUtMi4zNSAyLjQtMS42LTEuNiAyLjM1LTIuNC0zLjc1LTMuNzUtNC42IDQuNjVabTE2LjIgMTYuMjUgNC42NS00LjY1LTMuNzUtMy43NS0yLjQgMi40LTEuNi0xLjYgMi40LTIuNC00LjA1LTQtNC42IDQuNjVabS0yNS4xLS40NWgzLjZsMjAuNS0yMC40NS0zLjY1LTMuNjVMOS4xNSAzNS4yWiIvPjwvc3ZnPg==';
	Editor.thinGestureImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTI4IDQxLjZxLTIuNSAwLTQuMDc1LTEuNjUtMS41NzUtMS42NS0xLjU3NS00LjM1IDAtMi4zNSAxLjEtNC4xIDEuMS0xLjc1IDIuODI1LTIuOTUgMS43MjUtMS4yIDMuNy0xLjgyNVQzMy42NSAyNnEtLjE1LTMuMDUtMS4yNS00LjQyNVQyOS4yNSAyMC4ycS0yLjMgMC00LjE3NSAxLjV0LTQuNDI1IDQuOTVxLTIuNjUgMy40NS00LjYgNS4xLTEuOTUgMS42NS00LjM1IDEuNjUtMi4xIDAtMy42NS0xLjM3NVE2LjUgMzAuNjUgNi41IDI3LjQ1cTAtMiAxLjItNC41MjUgMS4yLTIuNTI1IDMuNS02LjE3NSAxLjMtMS44NSAxLjk3NS0zLjIyNXQuNjc1LTIuMzc1cTAtLjctLjM1LTEuMDUtLjM1LS4zNS0xLjA1LS4zNS0uNzUgMC0xLjYuNDc1UTEwIDEwLjcgOSAxMS45TDcgOS44cTEuMzUtMS41NSAyLjcyNS0yLjI3NVExMS4xIDYuOCAxMi41IDYuOHExLjkgMCAzLjEgMS4yNSAxLjIgMS4yNSAxLjIgMy4yIDAgMS44LS45NzUgMy41NzVRMTQuODUgMTYuNiAxMy4wNSAxOS4zcS0xLjk1IDIuOTUtMi43NzUgNC43dC0uODI1IDMuNjVxMCAxLjYuNzUgMi4yLjc1LjYgMS43NS42IDEuNDUgMCAyLjgyNS0xLjMgMS4zNzUtMS4zIDMuNTc1LTQuMTUgMi45NS0zLjggNS41MjUtNS43NzUgMi41NzUtMS45NzUgNS42NzUtMS45NzUgMy4xNSAwIDUgMi4zMjVUMzYuNiAyNS45aDQuOXYyLjk1aC00LjlxLS40NSA3LjM1LTMuMiAxMC4wNS0yLjc1IDIuNy01LjQgMi43Wm0uMS0yLjk1cTEuODUgMCAzLjY1LTIuMjUgMS44LTIuMjUgMi03LjUtMi44LjE1LTUuNjI1IDIuMDI1VDI1LjMgMzUuOXEwIDEuMy43NSAyLjAyNS43NS43MjUgMi4wNS43MjVaIi8+PC9zdmc+';
	Editor.thinShapesImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTI5Ljc1IDI5Ljc1Wm0tMTMgNS40NXEuNC4wNS43NS4wNUgxOS4wNXY0LjY1cTAgLjI1LjE3NS40MjV0LjQyNS4xNzVIMzkuOXEuMjUgMCAuNDI1LS4xNzV0LjE3NS0uNDI1VjE5LjY1cTAtLjI1LS4xNzUtLjQyNXQtLjQyNS0uMTc1aC00LjY1VjE3LjVxMC0uMzUtLjA1LS43NWg0LjdxMS4xNSAwIDIgLjg3NS44NS44NzUuODUgMi4wMjVWMzkuOXEwIDEuMTUtLjg1IDItLjg1Ljg1LTIgLjg1SDE5LjY1cS0xLjE1IDAtMi4wMjUtLjg1dC0uODc1LTJabTEuNS0zLjk1cS01LjQ1IDAtOS4yMjUtMy44LTMuNzc1LTMuOC0zLjc3NS05LjIgMC01LjQ1IDMuNzc1LTkuMjI1UTEyLjggNS4yNSAxOC4yNSA1LjI1cTUuNCAwIDkuMiAzLjc3NSAzLjggMy43NzUgMy44IDkuMjI1IDAgNS40LTMuOCA5LjItMy44IDMuOC05LjIgMy44Wm0tLjA1LTIuM3E0LjQ1IDAgNy42LTMuMTI1IDMuMTUtMy4xMjUgMy4xNS03LjU3NXQtMy4xMjUtNy42UTIyLjcgNy41IDE4LjI1IDcuNXQtNy42IDMuMTI1UTcuNSAxMy43NSA3LjUgMTguMnQzLjEyNSA3LjZxMy4xMjUgMy4xNSA3LjU3NSAzLjE1Wm0uMDUtMTAuN1oiLz48L3N2Zz4=';
	Editor.thinUndoImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTE0LjQgMzcuNXYtMi4yNWgxMy45cTMuNiAwIDYuMjI1LTIuMzc1UTM3LjE1IDMwLjUgMzcuMTUgMjYuOXEwLTMuNTUtMi42MjUtNS45LTIuNjI1LTIuMzUtNi4yMjUtMi4zNUgxMi45NUwxOSAyNC43bC0xLjYgMS42LTguOC04LjggOC44LTguOCAxLjYgMS42LTYuMDUgNi4wNWgxNS4zcTQuNTUgMCA3Ljg1IDMuMDV0My4zIDcuNXEwIDQuNS0zLjMgNy41NXQtNy44NSAzLjA1WiIvPjwvc3ZnPg==';
	Editor.thinRedoImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTE5Ljc1IDM3LjVxLTQuNTUgMC03Ljg1LTMuMDVUOC42IDI2LjlxMC00LjQ1IDMuMy03LjV0Ny44NS0zLjA1aDE1LjNMMjkgMTAuM2wxLjYtMS42IDguOCA4LjgtOC44IDguOC0xLjYtMS42IDYuMDUtNi4wNUgxOS43cS0zLjYgMC02LjIyNSAyLjM1LTIuNjI1IDIuMzUtMi42MjUgNS45IDAgMy42IDIuNjI1IDUuOTc1UTE2LjEgMzUuMjUgMTkuNyAzNS4yNWgxMy45djIuMjVaIi8+PC9zdmc+';
	Editor.thinDoubleArrowRightImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0ibTEzIDM1LjMtMS42LTEuNiA5Ljc1LTkuNzUtOS43NS05LjcgMS42LTEuNiAxMS4zNSAxMS4zWm0xMi4zIDAtMS42LTEuNiA5Ljc1LTkuNzUtOS43NS05LjcgMS42LTEuNiAxMS4zIDExLjNaIi8+PC9zdmc+';
	Editor.thinNoteImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgOTYgOTYwIDk2MCIgd2lkdGg9IjQ4Ij48cGF0aCBkPSJNMjM3LjY5NCA5NTUuOTk5cS0yMy41MjkgMC00MC42MTEtMTcuMDgyLTE3LjA4Mi0xNy4wODItMTcuMDgyLTQwLjYxMVYyNTMuNjk0cTAtMjMuNTI5IDE3LjA4Mi00MC42MTEgMTcuMDgyLTE3LjA4MiA0MC42MTEtMTcuMDgyaDM0Ny41MzdsMTk0Ljc2OCAxOTQuNzY4djUwNy41MzdxMCAyMy41MjktMTcuMDgyIDQwLjYxMS0xNy4wODIgMTcuMDgyLTQwLjYxMSAxNy4wODJIMjM3LjY5NFpNNTYyLjUzOSA0MTEuMjNWMjQxLjM4NUgyMzcuNjk0cS00LjYxNiAwLTguNDYzIDMuODQ2LTMuODQ2IDMuODQ3LTMuODQ2IDguNDYzdjY0NC42MTJxMCA0LjYxNiAzLjg0NiA4LjQ2MyAzLjg0NyAzLjg0NiA4LjQ2MyAzLjg0Nmg0ODQuNjEycTQuNjE2IDAgOC40NjMtMy44NDYgMy44NDYtMy44NDcgMy44NDYtOC40NjNWNDExLjIzSDU2Mi41MzlaTTIyNS4zODUgMjQxLjM4NVY0MTEuMjMgMjQxLjM4NXY2NjkuMjNWMjQxLjM4NVoiLz48L3N2Zz4=';
	Editor.thinTableImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTcgNDFWN2gzNHYzNFptMi4yNS0yMy42aDI5LjVWOS4yNUg5LjI1Wm0xMC42IDEwLjdoOC4zdi04LjRoLTguM1ptMCAxMC42NWg4LjN2LTguNGgtOC4zWk05LjI1IDI4LjFoOC4zNXYtOC40SDkuMjVabTIxLjE1IDBoOC4zNXYtOC40SDMwLjRaTTkuMjUgMzguNzVoOC4zNXYtOC40SDkuMjVabTIxLjE1IDBoOC4zNXYtOC40SDMwLjRaIi8+PC9zdmc+';
	Editor.thinAddCircleImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTIzIDMzLjVoMi4yNXYtOC4yaDguMjVWMjNoLTguMjV2LTguNUgyM1YyM2gtOC41djIuM0gyM1ptMSA5LjVxLTMuOTUgMC03LjQtMS41dC02LjAyNS00LjA3NVE4IDM0Ljg1IDYuNSAzMS40VDUgMjRxMC0zLjk1IDEuNS03LjQyNVE4IDEzLjEgMTAuNTc1IDEwLjU1IDEzLjE1IDggMTYuNiA2LjVUMjQgNXEzLjk1IDAgNy40MjUgMS41UTM0LjkgOCAzNy40NSAxMC41NSA0MCAxMy4xIDQxLjUgMTYuNTc1IDQzIDIwLjA1IDQzIDI0cTAgMy45NS0xLjUgNy40dC00LjA1IDYuMDI1UTM0LjkgNDAgMzEuNDI1IDQxLjUgMjcuOTUgNDMgMjQgNDNabS4wNS0yLjI1cTYuOTUgMCAxMS44MjUtNC45IDQuODc1LTQuOSA0Ljg3NS0xMS45IDAtNi45NS00Ljg3NS0xMS44MjVRMzEgNy4yNSAyNCA3LjI1cS02Ljk1IDAtMTEuODUgNC44NzVRNy4yNSAxNyA3LjI1IDI0cTAgNi45NSA0LjkgMTEuODUgNC45IDQuOSAxMS45IDQuOVpNMjQgMjRaIi8+PC9zdmc+';
	Editor.thinDeleteImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTEzLjkgNDFxLTEuMTUgMC0yLS44NS0uODUtLjg1LS44NS0yLjA1VjEwLjlIOVY4LjY1aDguNTV2LTEuNGgxMi45djEuNEgzOXYyLjI1aC0yLjA1djI3LjJxMCAxLjItLjg1IDIuMDUtLjg1Ljg1LTIgLjg1Wm0yMC44LTMwLjFIMTMuM3YyNy4ycTAgLjMuMTc1LjQ3NXQuNDI1LjE3NWgyMC4ycS4yIDAgLjQtLjJ0LjItLjQ1Wk0xOS4wNSAzNC41aDIuM1YxNS4xaC0yLjNabTcuNiAwaDIuM1YxNS4xaC0yLjNaTTEzLjMgMTAuOXYyNy44NVYzOC4xWiIvPjwvc3ZnPg==';
	Editor.thinLightImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTI0IDMwLjc1cTIuOCAwIDQuNzc1LTEuOTc1UTMwLjc1IDI2LjggMzAuNzUgMjRxMC0yLjgtMS45NzUtNC43NzVRMjYuOCAxNy4yNSAyNCAxNy4yNXEtMi44IDAtNC43NzUgMS45NzVRMTcuMjUgMjEuMiAxNy4yNSAyNHEwIDIuOCAxLjk3NSA0Ljc3NVEyMS4yIDMwLjc1IDI0IDMwLjc1Wk0yNCAzM3EtMy43NSAwLTYuMzc1LTIuNjI1VDE1IDI0cTAtMy43NSAyLjYyNS02LjM3NVQyNCAxNXEzLjc1IDAgNi4zNzUgMi42MjVUMzMgMjRxMCAzLjc1LTIuNjI1IDYuMzc1VDI0IDMzWk0zLjY1IDI1LjE1cS0uNSAwLS44MjUtLjMyNVEyLjUgMjQuNSAyLjUgMjRxMC0uNS4zMjUtLjgyNS4zMjUtLjMyNS44MjUtLjMyNWg1LjJxLjUgMCAuODI1LjMyNVExMCAyMy41IDEwIDI0cTAgLjUtLjMyNS44MjUtLjMyNS4zMjUtLjgyNS4zMjVabTM1LjUgMHEtLjUgMC0uODI1LS4zMjVRMzggMjQuNSAzOCAyNHEwLS41LjMyNS0uODI1LjMyNS0uMzI1LjgyNS0uMzI1aDUuMnEuNSAwIC44MjUuMzI1LjMyNS4zMjUuMzI1LjgyNSAwIC41LS4zMjUuODI1LS4zMjUuMzI1LS44MjUuMzI1Wk0yNCAxMHEtLjUgMC0uODI1LS4zMjUtLjMyNS0uMzI1LS4zMjUtLjgyNXYtNS4ycTAtLjUuMzI1LS44MjVRMjMuNSAyLjUgMjQgMi41cS41IDAgLjgyNS4zMjUuMzI1LjMyNS4zMjUuODI1djUuMnEwIC41LS4zMjUuODI1UTI0LjUgMTAgMjQgMTBabTAgMzUuNXEtLjUgMC0uODI1LS4zMjUtLjMyNS0uMzI1LS4zMjUtLjgyNXYtNS4ycTAtLjUuMzI1LS44MjVRMjMuNSAzOCAyNCAzOHEuNSAwIC44MjUuMzI1LjMyNS4zMjUuMzI1LjgyNXY1LjJxMCAuNS0uMzI1LjgyNS0uMzI1LjMyNS0uODI1LjMyNVpNMTIuNSAxNC4xbC0zLTIuOTVxLS4zNS0uMzUtLjMyNS0uODI1UTkuMiA5Ljg1IDkuNSA5LjVxLjM1LS4zNS44LS4zNS40NSAwIC44NS4zNWwyLjk1IDNxLjMuMzUuMy44IDAgLjQ1LS4zLjgtLjMuMy0uNzc1LjMtLjQ3NSAwLS44MjUtLjNabTI0LjM1IDI0LjQtMi45NS0zcS0uMy0uMzUtLjMtLjggMC0uNDUuMzUtLjguMjUtLjM1LjcyNS0uMzV0LjgyNS4zNWwzIDIuOTVxLjM1LjM1LjMyNS44MjUtLjAyNS40NzUtLjMyNS44MjUtLjM1LjM1LS44LjM1LS40NSAwLS44NS0uMzVaTTMzLjkgMTQuMXEtLjM1LS4zNS0uMzUtLjggMC0uNDUuMzUtLjhsMi45NS0zcS4zNS0uMzUuODI1LS4zMjUuNDc1LjAyNS44MjUuMzI1LjM1LjM1LjM1LjggMCAuNDUtLjM1Ljg1bC0zIDIuOTVxLS4zLjMtLjc3NS4zLS40NzUgMC0uODI1LS4zWk05LjUgMzguNXEtLjM1LS4zNS0uMzUtLjggMC0uNDUuMzUtLjg1bDMtMi45NXEuMzUtLjM1LjgtLjM1LjQ1IDAgLjguMzUuMzUuMy4zMjUuNzc1LS4wMjUuNDc1LS4zMjUuODI1bC0yLjk1IDNxLS40LjM1LS44NS4zNS0uNDUgMC0uOC0uMzVaTTI0IDI0WiIvPjwvc3ZnPg==';
	Editor.thinDarkImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTI0LjA1IDQxcS03LjEgMC0xMi4wNS00Ljk1UTcuMDUgMzEuMSA3LjA1IDI0cTAtNi44IDQuNi0xMS42NSA0LjYtNC44NSAxMS4zLTUuMjUuMiAwIC40NS4wMjV0LjcuMDI1UTIyLjc1IDguNyAyMiAxMC43MjVxLS43NSAyLjAyNS0uNzUgNC4yNzUgMCA0LjkgMy40NSA4LjM1IDMuNDUgMy40NSA4LjM1IDMuNDUgMi4yIDAgNC4yNzUtLjY3NVQ0MC45IDI0LjJxMCAuMzUuMDI1LjU1LjAyNS4yLjAyNS4zNS0uNCA2LjctNS4yNSAxMS4zUTMwLjg1IDQxIDI0LjA1IDQxWm0wLTIuMjVxNS4xNSAwIDkuMDc1LTMuMTI1UTM3LjA1IDMyLjUgMzguMiAyOC4xcS0xLjIuNS0yLjUuNzI1LTEuMy4yMjUtMi42NS4yMjUtNS44NSAwLTkuOTUtNC4xVDE5IDE1cTAtMS4xNS4yMjUtMi40MjVUMjAgOS43NXEtNC42NSAxLjQtNy42NSA1LjM3NVQ5LjM1IDI0cTAgNi4xNSA0LjI3NSAxMC40NSA0LjI3NSA0LjMgMTAuNDI1IDQuM1ptLS4yNS0xNC41WiIvPjwvc3ZnPg==';
	Editor.thinCommentImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTEyLjUgMjcuNWgyM3YtMi4yNWgtMjNabTAtNi4zNWgyM3YtMi4zaC0yM1ptMC02LjRoMjNWMTIuNWgtMjNaTTQzIDQyLjEgMzUuOSAzNWgtMjhxLTEuMTUgMC0yLjAyNS0uODc1VDUgMzIuMVY3LjlxMC0xLjE1Ljg3NS0yLjAyNVQ3LjkgNWgzMi4ycTEuMiAwIDIuMDUuODc1UTQzIDYuNzUgNDMgNy45Wk03LjI1IDcuOXYyNC44NUgzNi45bDMuODUgMy44NVY3LjlxMC0uMy0uMTc1LS40NzVUNDAuMSA3LjI1SDcuOXEtLjMgMC0uNDc1LjE3NVQ3LjI1IDcuOVptMCAwdjI4LjdWNy4yNSA3LjlaIi8+PC9zdmc+';
	Editor.thinMenuImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTEzLjUgMjYuMTVxLjkgMCAxLjUyNS0uNjI1LjYyNS0uNjI1LjYyNS0xLjUyNSAwLS45LS42MjUtMS41MjUtLjYyNS0uNjI1LTEuNTI1LS42MjUtLjkgMC0xLjUyNS42MjUtLjYyNS42MjUtLjYyNSAxLjUyNSAwIC45LjYyNSAxLjUyNS42MjUuNjI1IDEuNTI1LjYyNVptMTAuNSAwcS45IDAgMS41MjUtLjYyNS42MjUtLjYyNS42MjUtMS41MjUgMC0uOS0uNjI1LTEuNTI1UTI0LjkgMjEuODUgMjQgMjEuODVxLS45IDAtMS41MjUuNjI1LS42MjUuNjI1LS42MjUgMS41MjUgMCAuOS42MjUgMS41MjUuNjI1LjYyNSAxLjUyNS42MjVabTEwLjUgMHEuODUgMCAxLjQ3NS0uNjI1UTM2LjYgMjQuOSAzNi42IDI0cTAtLjktLjYyNS0xLjUyNS0uNjI1LS42MjUtMS41MjUtLjYyNS0uODUgMC0xLjQ3NS42MjUtLjYyNS42MjUtLjYyNSAxLjUyNSAwIC45LjYyNSAxLjUyNS42MjUuNjI1IDEuNTI1LjYyNVpNMjQgNDNxLTMuOTUgMC03LjQtMS41dC02LjAyNS00LjA3NVE4IDM0Ljg1IDYuNSAzMS40VDUgMjRxMC0zLjk1IDEuNS03LjQyNVE4IDEzLjEgMTAuNTc1IDEwLjU1IDEzLjE1IDggMTYuNiA2LjVUMjQgNXEzLjk1IDAgNy40MjUgMS41UTM0LjkgOCAzNy40NSAxMC41NSA0MCAxMy4xIDQxLjUgMTYuNTc1IDQzIDIwLjA1IDQzIDI0cTAgMy45NS0xLjUgNy40dC00LjA1IDYuMDI1UTM0LjkgNDAgMzEuNDI1IDQxLjUgMjcuOTUgNDMgMjQgNDNabTAtMi4yNXE3IDAgMTEuODc1LTQuOVQ0MC43NSAyNHEwLTctNC44NzUtMTEuODc1VDI0IDcuMjVxLTYuOTUgMC0xMS44NSA0Ljg3NVE3LjI1IDE3IDcuMjUgMjRxMCA2Ljk1IDQuOSAxMS44NSA0LjkgNC45IDExLjg1IDQuOVpNMjQgMjRaIi8+PC9zdmc+';
	Editor.thinViewImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTUgMzlWOWgzOHYzMFptMjguNTUtMjAuNmg3LjJ2LTcuMTVoLTcuMlptMCA4Ljk1aDcuMnYtNi43aC03LjJabS0yNi4zIDkuNEgzMS4zdi0yNS41SDcuMjVabTI2LjMgMGg3LjJWMjkuNmgtNy4yWiIvPjwvc3ZnPg==';
	Editor.thinUserAddImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTM2LjYgMjcuNXYtNi4zNWgtNi4zNXYtMi4zaDYuMzVWMTIuNWgyLjN2Ni4zNWg2LjM1djIuM0gzOC45djYuMzVaTTE4IDIzLjM1cS0yLjkgMC00Ljc3NS0xLjg3NVExMS4zNSAxOS42IDExLjM1IDE2LjdxMC0yLjkgMS44NzUtNC43NVQxOCAxMC4xcTIuOSAwIDQuNzc1IDEuODUgMS44NzUgMS44NSAxLjg3NSA0Ljc1dC0xLjg3NSA0Ljc3NVEyMC45IDIzLjM1IDE4IDIzLjM1Wk0zIDM4LjZ2LTMuOHEwLTEuNS44LTIuNzV0Mi4yNS0xLjlxMy40NS0xLjUgNi4yNzUtMi4xNSAyLjgyNS0uNjUgNS42NzUtLjY1IDIuODUgMCA1LjY1LjY1IDIuOC42NSA2LjI1IDIuMTUgMS40NS43IDIuMjc1IDEuOTI1VDMzIDM0Ljh2My44Wm0yLjI1LTIuMjVoMjUuNVYzNC44cTAtLjc1LS41LTEuNDc1LS41LS43MjUtMS4zLTEuMTI1LTMuMi0xLjUtNS42NzUtMi4wNVEyMC44IDI5LjYgMTggMjkuNnEtMi44IDAtNS4zLjU1VDcgMzIuMnEtLjguNC0xLjI3NSAxLjEyNS0uNDc1LjcyNS0uNDc1IDEuNDc1Wk0xOCAyMS4xcTEuODUgMCAzLjEtMS4yNXQxLjI1LTMuMTVxMC0xLjg1LTEuMjUtMy4xVDE4IDEyLjM1cS0xLjg1IDAtMy4xIDEuMjV0LTEuMjUgMy4xcTAgMS45IDEuMjUgMy4xNVQxOCAyMS4xWm0wLTQuNFptMCAxOS42NVoiLz48L3N2Zz4=';
	Editor.thinUserFlashImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTkgMzguNnYtMy44cTAtMS42Ljg1LTIuOC44NS0xLjIgMi4yLTEuODUgMy4yLTEuNCA2LjEyNS0yLjEgMi45MjUtLjcgNS44MjUtLjcgMS40NSAwIDIuOS4xNzV0Mi45LjUyNXYyLjJxLTEuNDUtLjM1LTIuODc1LS41UTI1LjUgMjkuNiAyNCAyOS42cS0yLjc1IDAtNS40LjYtMi42NS42LTUuNiAyLS43NS40LTEuMjUgMS4xMjV0LS41IDEuNDc1djEuNTVIMjkuOHYyLjI1Wm0yLjI1LTIuMjVIMjkuOFptMTIuNzUtMTNxLTIuOSAwLTQuNzc1LTEuODc1UTE3LjM1IDE5LjYgMTcuMzUgMTYuN3EwLTIuOSAxLjg3NS00Ljc1VDI0IDEwLjFxMi45IDAgNC43NzUgMS44NSAxLjg3NSAxLjg1IDEuODc1IDQuNzV0LTEuODc1IDQuNzc1UTI2LjkgMjMuMzUgMjQgMjMuMzVabTAtMi4yNXExLjg1IDAgMy4xLTEuMjV0MS4yNS0zLjE1cTAtMS44NS0xLjI1LTMuMVQyNCAxMi4zNXEtMS44NSAwLTMuMSAxLjI1dC0xLjI1IDMuMXEwIDEuOSAxLjI1IDMuMTVUMjQgMjEuMVptMC00LjRabTEyLjg1IDI4LjA1di03LjhoLTMuNHYtMTAuMWg5LjE1bC0zLjggNy42NWgzLjdaIi8+PC9zdmc+';
	Editor.thinShareImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTExLjkgNDVxLTEuMiAwLTIuMDUtLjg1UTkgNDMuMyA5IDQyLjFWMTguOHEwLTEuMTUuODUtMiAuODUtLjg1IDIuMDUtLjg1aDYuOXYyLjI1aC02LjlxLS4yNSAwLS40NS4ydC0uMi40djIzLjNxMCAuMjUuMi40NXQuNDUuMmgyNC4ycS4yNSAwIC40NS0uMnQuMi0uNDVWMTguOHEwLS4yLS4yLS40dC0uNDUtLjJoLTYuOTV2LTIuMjVoNi45NXExLjIgMCAyLjA1Ljg1Ljg1Ljg1Ljg1IDJ2MjMuM3EwIDEuMi0uODUgMi4wNS0uODUuODUtMi4wNS44NVptMTAuOTUtMTQuNVY4LjFsLTQuNiA0LjU1LTEuNjUtMS42IDcuMzUtNy4zNSA3LjM1IDcuMzUtMS42IDEuNi00LjYtNC41NXYyMi40WiIvPjwvc3ZnPg==';
	Editor.thinTextImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTMuNCA0NC42di05LjI1aDMuNTV2LTIyLjdIMy40VjMuNGg5LjI1djMuNTVoMjIuN1YzLjRoOS4yNXY5LjI1aC0zLjU1djIyLjdoMy41NXY5LjI1aC05LjI1di0zLjU1aC0yMi43djMuNTVabTkuMjUtNS44NWgyMi43di0zLjRoMy40di0yMi43aC0zLjR2LTMuNGgtMjIuN3YzLjRoLTMuNHYyMi43aDMuNFptMy4xNS02LjI1IDcuMzUtMTkuMTVoMS42NWw3LjQ1IDE5LjE1aC0yLjFMMjggMjdoLTcuODVsLTIuMSA1LjVabTQuOTUtNy4zNWg2LjVMMjQuMSAxNi44aC0uM1ptLTE1LjEtMTQuOGg0Ljd2LTQuN2gtNC43Wm0zMiAwaDQuN3YtNC43aC00LjdabTAgMzJoNC43di00LjdoLTQuN1ptLTMyIDBoNC43di00LjdoLTQuN1ptMzItMzJabTAgMjcuM1ptLTI3LjMgMFptMC0yNy4zWiIvPjwvc3ZnPg==';
	Editor.thinRectangleImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTUgMzlWOWgzOHYzMFptMi4yNS0yLjI1aDMzLjV2LTI1LjVINy4yNVptMCAwdi0yNS41IDI1LjVaIi8+PC9zdmc+';
	Editor.thinDataImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTI4Ljg1IDM5LjF2LTIuMjVIMzRxMS4yIDAgMi4wMjUtLjgyNVQzNi44NSAzNHYtNC45cTAtMS43NSAxLjA3NS0zLjEyNXQyLjcyNS0xLjgyNXYtLjNxLTEuNjUtLjQ1LTIuNzI1LTEuODI1UTM2Ljg1IDIwLjY1IDM2Ljg1IDE4LjlWMTRxMC0xLjItLjgyNS0yLjAyNVQzNCAxMS4xNWgtNS4xNVY4LjlIMzRxMi4xNSAwIDMuNjI1IDEuNVQzOS4xIDE0djQuOXEwIDEuMjUuODUgMi4wNzUuODUuODI1IDIuMS44MjVoLjg1djQuNGgtLjg1cS0xLjI1IDAtMi4xLjgyNS0uODUuODI1LS44NSAyLjA3NVYzNHEwIDIuMS0xLjUgMy42VDM0IDM5LjFaTTE0IDM5LjFxLTIuMTUgMC0zLjYyNS0xLjVUOC45IDM0di00LjlxMC0xLjI1LS44NS0yLjA3NS0uODUtLjgyNS0yLjEtLjgyNUg1LjF2LTQuNGguODVxMS4yNSAwIDIuMS0uODI1Ljg1LS44MjUuODUtMi4wNzVWMTRxMC0yLjEgMS41LTMuNlQxNCA4LjloNS4xNXYyLjI1SDE0cS0xLjIgMC0yLjAyNS44MjVUMTEuMTUgMTR2NC45cTAgMS43NS0xLjA3NSAzLjEyNVQ3LjM1IDIzLjg1di4zcTEuNjUuNDUgMi43MjUgMS44MjVRMTEuMTUgMjcuMzUgMTEuMTUgMjkuMVYzNHEwIDEuMi44MjUgMi4wMjVUMTQgMzYuODVoNS4xNXYyLjI1WiIvPjwvc3ZnPg==';
	Editor.thinExpandImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHdpZHRoPSI0OCI+PHBhdGggZD0iTTI0IDMwLjEgMTIuNyAxOC43NWwxLjYtMS42IDkuNyA5LjcgOS43LTkuNyAxLjYgMS42NVoiLz48L3N2Zz4=';
	Editor.thinGridImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iNDgiPjxwYXRoIGQ9Ik0yMjMuMjkxLTE1NC41cS0yOS4xMTcgMC00OC45NTQtMTkuODM3VDE1NC41LTIyMy4yOTFxMC0yOS4xMTggMTkuODM3LTQ4LjkxM1ExOTQuMTc0LTI5MiAyMjMuMjkxLTI5MnEyOS4xMTggMCA0OC45MTMgMTkuNzk2UTI5Mi0yNTIuNDA5IDI5Mi0yMjMuMjkxcTAgMjkuMTE3LTE5Ljc5NiA0OC45NTQtMTkuNzk1IDE5LjgzNy00OC45MTMgMTkuODM3Wm0yNTYuOCAwcS0yOS4wNDggMC00OC44ODUtMTkuODM3LTE5LjgzNi0xOS44MzctMTkuODM2LTQ4Ljk1NCAwLTI5LjExOCAxOS43NDUtNDguOTEzUTQ1MC44NjEtMjkyIDQ3OS45MDktMjkydDQ4Ljg4NSAxOS43OTZxMTkuODM2IDE5Ljc5NSAxOS44MzYgNDguOTEzIDAgMjkuMTE3LTE5Ljc0NSA0OC45NTQtMTkuNzQ2IDE5LjgzNy00OC43OTQgMTkuODM3Wm0yNTYuNjE4IDBxLTI5LjExOCAwLTQ4LjkxMy0xOS44MzdRNjY4LTE5NC4xNzQgNjY4LTIyMy4yOTFxMC0yOS4xMTggMTkuNzk2LTQ4LjkxM1E3MDcuNTkxLTI5MiA3MzYuNzA5LTI5MnEyOS4xMTcgMCA0OC45NTQgMTkuNzk2IDE5LjgzNyAxOS43OTUgMTkuODM3IDQ4LjkxMyAwIDI5LjExNy0xOS44MzcgNDguOTU0VDczNi43MDktMTU0LjVaTTIyMy4yOTEtNDExLjM3cS0yOS4xMTcgMC00OC45NTQtMTkuNzQ1LTE5LjgzNy0xOS43NDYtMTkuODM3LTQ4Ljc5NHQxOS44MzctNDguODg1cTE5LjgzNy0xOS44MzYgNDguOTU0LTE5LjgzNiAyOS4xMTggMCA0OC45MTMgMTkuNzQ1UTI5Mi01MDkuMTM5IDI5Mi00ODAuMDkxdC0xOS43OTYgNDguODg1cS0xOS43OTUgMTkuODM2LTQ4LjkxMyAxOS44MzZabTI1Ni44IDBxLTI5LjA0OCAwLTQ4Ljg4NS0xOS43NDUtMTkuODM2LTE5Ljc0Ni0xOS44MzYtNDguNzk0dDE5Ljc0NS00OC44ODVxMTkuNzQ2LTE5LjgzNiA0OC43OTQtMTkuODM2dDQ4Ljg4NSAxOS43NDVxMTkuODM2IDE5Ljc0NiAxOS44MzYgNDguNzk0dC0xOS43NDUgNDguODg1cS0xOS43NDYgMTkuODM2LTQ4Ljc5NCAxOS44MzZabTI1Ni42MTggMHEtMjkuMTE4IDAtNDguOTEzLTE5Ljc0NVE2NjgtNDUwLjg2MSA2NjgtNDc5LjkwOXQxOS43OTYtNDguODg1cTE5Ljc5NS0xOS44MzYgNDguOTEzLTE5LjgzNiAyOS4xMTcgMCA0OC45NTQgMTkuNzQ1IDE5LjgzNyAxOS43NDYgMTkuODM3IDQ4Ljc5NHQtMTkuODM3IDQ4Ljg4NXEtMTkuODM3IDE5LjgzNi00OC45NTQgMTkuODM2Wk0yMjMuMjkxLTY2OHEtMjkuMTE3IDAtNDguOTU0LTE5Ljc5Ni0xOS44MzctMTkuNzk1LTE5LjgzNy00OC45MTMgMC0yOS4xMTcgMTkuODM3LTQ4Ljk1NHQ0OC45NTQtMTkuODM3cTI5LjExOCAwIDQ4LjkxMyAxOS44MzdRMjkyLTc2NS44MjYgMjkyLTczNi43MDlxMCAyOS4xMTgtMTkuNzk2IDQ4LjkxM1EyNTIuNDA5LTY2OCAyMjMuMjkxLTY2OFptMjU2LjggMHEtMjkuMDQ4IDAtNDguODg1LTE5Ljc5Ni0xOS44MzYtMTkuNzk1LTE5LjgzNi00OC45MTMgMC0yOS4xMTcgMTkuNzQ1LTQ4Ljk1NCAxOS43NDYtMTkuODM3IDQ4Ljc5NC0xOS44Mzd0NDguODg1IDE5LjgzN3ExOS44MzYgMTkuODM3IDE5LjgzNiA0OC45NTQgMCAyOS4xMTgtMTkuNzQ1IDQ4LjkxM1E1MDkuMTM5LTY2OCA0ODAuMDkxLTY2OFptMjU2LjYxOCAwcS0yOS4xMTggMC00OC45MTMtMTkuNzk2UTY2OC03MDcuNTkxIDY2OC03MzYuNzA5cTAtMjkuMTE3IDE5Ljc5Ni00OC45NTQgMTkuNzk1LTE5LjgzNyA0OC45MTMtMTkuODM3IDI5LjExNyAwIDQ4Ljk1NCAxOS44Mzd0MTkuODM3IDQ4Ljk1NHEwIDI5LjExOC0xOS44MzcgNDguOTEzUTc2NS44MjYtNjY4IDczNi43MDktNjY4WiIvPjwvc3ZnPg==';
	Editor.thinOpenImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIiB3aWR0aD0iMjQiPjxwYXRoIGQ9Ik0yNDAtODBxLTMzIDAtNTYuNS0yMy41VDE2MC0xNjB2LTY0MHEwLTMzIDIzLjUtNTYuNVQyNDAtODgwaDMyMGwyNDAgMjQwdjI0MGgtODB2LTIwMEg1MjB2LTIwMEgyNDB2NjQwaDM2MHY4MEgyNDBabTYzOCAxNUw3NjAtMTgzdjg5aC04MHYtMjI2aDIyNnY4MGgtOTBsMTE4IDExOC01NiA1N1ptLTYzOC05NXYtNjQwIDY0MFoiLz48L3N2Zz4=';
	Editor.selectImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNMjAwLTEyMHEtMzMgMC01Ni41LTIzLjVUMTIwLTIwMHYtNTYwcTAtMzMgMjMuNS01Ni41VDIwMC04NDBoNTYwcTggMCAxNSAxLjV0MTQgNC41bC03NCA3NEgyMDB2NTYwaDU2MHYtMjY2bDgwLTgwdjM0NnEwIDMzLTIzLjUgNTYuNVQ3NjAtMTIwSDIwMFptMjYxLTE2MEwyMzUtNTA2bDU2LTU2IDE3MCAxNzAgMzY3LTM2NyA1NyA1NS00MjQgNDI0WiIvPjwvc3ZnPg==';

	/**
	 * 
	 */
	Editor.styles = [{},
		{commonStyle: {fontColor: '#393C56', strokeColor: '#E07A5F', fillColor: '#F2CC8F'},	
			graph: {background: '#F4F1DE', gridColor: '#D4D0C0'}},
		{vertexStyle: {strokeColor: '#BAC8D3', fillColor: '#09555B', fontColor: '#EEEEEE'},
			edgeStyle: {strokeColor: '#0B4D6A'}},
		{vertexStyle: {strokeColor: '#FFFFFF', fillColor: '#182E3E', fontColor: '#FFFFFF'},
			edgeStyle: {strokeColor: '#23445D'},
			graph: {background: '#FCE7CD', gridColor: '#CFBDA8'}},
		{vertexStyle: {strokeColor: '#D0CEE2', fillColor: '#5D7F99'},
			edgeStyle: {strokeColor: '#736CA8'},
			commonStyle: {fontColor: '#1A1A1A'}},
		{commonStyle: {fontColor: '#46495D', strokeColor: '#788AA3', fillColor: '#B2C9AB'}},
		{commonStyle: {fontColor: '#5AA9E6', strokeColor: '#FF6392', fillColor: '#FFE45E'}},
		{commonStyle: {fontColor: '#E4FDE1', strokeColor: '#028090', fillColor: '#F45B69'},
			graph: {background: '#114B5F', gridColor: '#0B3240'}},
		{commonStyle: {fontColor: '#FEFAE0', strokeColor: '#DDA15E', fillColor: '#BC6C25'},
			graph: {background: '#283618', gridColor: '#48632C'}},
			{commonStyle: {fontColor: '#143642', strokeColor: '#0F8B8D', fillColor: '#FAE5C7'},
			edgeStyle: {strokeColor: '#A8201A'},
			graph: {background: '#DAD2D8', gridColor: '#ABA4A9'}},
		{},
		{vertexStyle: {strokeColor: '#D0CEE2', fillColor: '#FAD9D5'},
			edgeStyle: {strokeColor: '#09555B'},
			commonStyle: {fontColor: '#1A1A1A'}},
		{commonStyle: {fontColor: '#1D3557', strokeColor: '#457B9D', fillColor: '#A8DADC'},	
			graph: {background: '#F1FAEE'}},
		{commonStyle: {fontColor: '#095C86', strokeColor: '#AF45ED', fillColor: '#F694C1'},
			edgeStyle: {strokeColor: '#60E696'}},
		{commonStyle: {fontColor: '#5C5C5C', strokeColor: '#006658', fillColor: '#21C0A5'}},
		{vertexStyle: {strokeColor: '#FFFFFF', fillColor: '#F08E81'},
			edgeStyle: {strokeColor: '#182E3E'},
			commonStyle: {fontColor: '#1A1A1A'},
			graph: {background: '#B0E3E6', gridColor: '#87AEB0'}},
		{vertexStyle: {strokeColor: '#909090', fillColor: '#F5AB50'},
			edgeStyle: {strokeColor: '#182E3E'},
			commonStyle: {fontColor: '#1A1A1A'},
			graph: {background: '#EEEEEE'}},
		{vertexStyle: {strokeColor: '#BAC8D3', fillColor: '#B1DDF0', fontColor: '#182E3E'},
			edgeStyle: {strokeColor: '#EEEEEE', fontColor: '#FFFFFF'},
			graph: {background: '#09555B', gridColor: '#13B4C2'}},
		{vertexStyle: {strokeColor: '#EEEEEE', fillColor: '#56517E', fontColor: '#FFFFFF'},
			edgeStyle: {strokeColor: '#182E3E'},
			graph: {background: '#FAD9D5', gridColor: '#BFA6A3'}},
		{vertexStyle: {fillColor: '#EEEEEE', fontColor: '#1A1A1A'},
			edgeStyle: {fontColor: '#FFFFFF'},
			commonStyle: {strokeColor: '#FFFFFF'},
			graph: {background: '#182E3E', gridColor: '#4D94C7'}}];
	
	/**
	 * 
	 */
	Editor.logoImage = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzA2LjE4NSAxMjAuMjk2IgogICB2aWV3Qm94PSIyNCAyNiA2OCA2OCIKICAgeT0iMHB4IgogICB4PSIwcHgiCiAgIHZlcnNpb249IjEuMSI+CiAgIAkgPGc+PGxpbmUKICAgICAgIHkyPSI3Mi4zOTQiCiAgICAgICB4Mj0iNDEuMDYxIgogICAgICAgeTE9IjQzLjM4NCIKICAgICAgIHgxPSI1OC4wNjkiCiAgICAgICBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiCiAgICAgICBzdHJva2Utd2lkdGg9IjMuNTUyOCIKICAgICAgIHN0cm9rZT0iI0ZGRkZGRiIKICAgICAgIGZpbGw9Im5vbmUiIC8+PGxpbmUKICAgICAgIHkyPSI3Mi4zOTQiCiAgICAgICB4Mj0iNzUuMDc2IgogICAgICAgeTE9IjQzLjM4NCIKICAgICAgIHgxPSI1OC4wNjgiCiAgICAgICBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiCiAgICAgICBzdHJva2Utd2lkdGg9IjMuNTAwOCIKICAgICAgIHN0cm9rZT0iI0ZGRkZGRiIKICAgICAgIGZpbGw9Im5vbmUiIC8+PGc+PHBhdGgKICAgICAgICAgZD0iTTUyLjc3Myw3Ny4wODRjMCwxLjk1NC0xLjU5OSwzLjU1My0zLjU1MywzLjU1M0gzNi45OTljLTEuOTU0LDAtMy41NTMtMS41OTktMy41NTMtMy41NTN2LTkuMzc5ICAgIGMwLTEuOTU0LDEuNTk5LTMuNTUzLDMuNTUzLTMuNTUzaDEyLjIyMmMxLjk1NCwwLDMuNTUzLDEuNTk5LDMuNTUzLDMuNTUzVjc3LjA4NHoiCiAgICAgICAgIGZpbGw9IiNGRkZGRkYiIC8+PC9nPjxnCiAgICAgICBpZD0iZzM0MTkiPjxwYXRoCiAgICAgICAgIGQ9Ik02Ny43NjIsNDguMDc0YzAsMS45NTQtMS41OTksMy41NTMtMy41NTMsMy41NTNINTEuOTg4Yy0xLjk1NCwwLTMuNTUzLTEuNTk5LTMuNTUzLTMuNTUzdi05LjM3OSAgICBjMC0xLjk1NCwxLjU5OS0zLjU1MywzLjU1My0zLjU1M0g2NC4yMWMxLjk1NCwwLDMuNTUzLDEuNTk5LDMuNTUzLDMuNTUzVjQ4LjA3NHoiCiAgICAgICAgIGZpbGw9IiNGRkZGRkYiIC8+PC9nPjxnPjxwYXRoCiAgICAgICAgIGQ9Ik04Mi43NTIsNzcuMDg0YzAsMS45NTQtMS41OTksMy41NTMtMy41NTMsMy41NTNINjYuOTc3Yy0xLjk1NCwwLTMuNTUzLTEuNTk5LTMuNTUzLTMuNTUzdi05LjM3OSAgICBjMC0xLjk1NCwxLjU5OS0zLjU1MywzLjU1My0zLjU1M2gxMi4yMjJjMS45NTQsMCwzLjU1MywxLjU5OSwzLjU1MywzLjU1M1Y3Ny4wODR6IgogICAgICAgICBmaWxsPSIjRkZGRkZGIiAvPjwvZz48L2c+PC9zdmc+';
	Editor.saveImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iYmxhY2siIHdpZHRoPSIxOHB4IiBoZWlnaHQ9IjE4cHgiPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMTkgMTJ2N0g1di03SDN2N2MwIDEuMS45IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0ydi03aC0yem0tNiAuNjdsMi41OS0yLjU4TDE3IDExLjVsLTUgNS01LTUgMS40MS0xLjQxTDExIDEyLjY3VjNoMnoiLz48L3N2Zz4=';
	Editor.globeImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTEuOTkgMkM2LjQ3IDIgMiA2LjQ4IDIgMTJzNC40NyAxMCA5Ljk5IDEwQzE3LjUyIDIyIDIyIDE3LjUyIDIyIDEyUzE3LjUyIDIgMTEuOTkgMnptNi45MyA2aC0yLjk1Yy0uMzItMS4yNS0uNzgtMi40NS0xLjM4LTMuNTYgMS44NC42MyAzLjM3IDEuOTEgNC4zMyAzLjU2ek0xMiA0LjA0Yy44MyAxLjIgMS40OCAyLjUzIDEuOTEgMy45NmgtMy44MmMuNDMtMS40MyAxLjA4LTIuNzYgMS45MS0zLjk2ek00LjI2IDE0QzQuMSAxMy4zNiA0IDEyLjY5IDQgMTJzLjEtMS4zNi4yNi0yaDMuMzhjLS4wOC42Ni0uMTQgMS4zMi0uMTQgMiAwIC42OC4wNiAxLjM0LjE0IDJINC4yNnptLjgyIDJoMi45NWMuMzIgMS4yNS43OCAyLjQ1IDEuMzggMy41Ni0xLjg0LS42My0zLjM3LTEuOS00LjMzLTMuNTZ6bTIuOTUtOEg1LjA4Yy45Ni0xLjY2IDIuNDktMi45MyA0LjMzLTMuNTZDOC44MSA1LjU1IDguMzUgNi43NSA4LjAzIDh6TTEyIDE5Ljk2Yy0uODMtMS4yLTEuNDgtMi41My0xLjkxLTMuOTZoMy44MmMtLjQzIDEuNDMtMS4wOCAyLjc2LTEuOTEgMy45NnpNMTQuMzQgMTRIOS42NmMtLjA5LS42Ni0uMTYtMS4zMi0uMTYtMiAwLS42OC4wNy0xLjM1LjE2LTJoNC42OGMuMDkuNjUuMTYgMS4zMi4xNiAyIDAgLjY4LS4wNyAxLjM0LS4xNiAyem0uMjUgNS41NmMuNi0xLjExIDEuMDYtMi4zMSAxLjM4LTMuNTZoMi45NWMtLjk2IDEuNjUtMi40OSAyLjkzLTQuMzMgMy41NnpNMTYuMzYgMTRjLjA4LS42Ni4xNC0xLjMyLjE0LTIgMC0uNjgtLjA2LTEuMzQtLjE0LTJoMy4zOGMuMTYuNjQuMjYgMS4zMS4yNiAycy0uMSAxLjM2LS4yNiAyaC0zLjM4eiIvPjwvc3ZnPg==';
	Editor.commentImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNMjQwLTQwMGg0ODB2LTgwSDI0MHY4MFptMC0xMjBoNDgwdi04MEgyNDB2ODBabTAtMTIwaDQ4MHYtODBIMjQwdjgwWk04ODAtODAgNzIwLTI0MEgxNjBxLTMzIDAtNTYuNS0yMy41VDgwLTMyMHYtNDgwcTAtMzMgMjMuNS01Ni41VDE2MC04ODBoNjQwcTMzIDAgNTYuNSAyMy41VDg4MC04MDB2NzIwWk0xNjAtMzIwaDU5NGw0NiA0NXYtNTI1SDE2MHY0ODBabTAgMHYtNDgwIDQ4MFoiLz48L3N2Zz4=';
	Editor.userImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMTJjMi4yMSAwIDQtMS43OSA0LTRzLTEuNzktNC00LTQtNCAxLjc5LTQgNCAxLjc5IDQgNCA0em0wIDJjLTIuNjcgMC04IDEuMzQtOCA0djJoMTZ2LTJjMC0yLjY2LTUuMzMtNC04LTR6Ii8+PC9zdmc+';
	Editor.groupImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDAtMTYwdi0xMTJxMC0zNCAxNy41LTYyLjVUMTA0LTM3OHE2Mi0zMSAxMjYtNDYuNVQzNjAtNDQwcTY2IDAgMTMwIDE1LjVUNjE2LTM3OHEyOSAxNSA0Ni41IDQzLjVUNjgwLTI3MnYxMTJINDBabTcyMCAwdi0xMjBxMC00NC0yNC41LTg0LjVUNjY2LTQzNHE1MSA2IDk2IDIwLjV0ODQgMzUuNXEzNiAyMCA1NSA0NC41dDE5IDUzLjV2MTIwSDc2MFpNMzYwLTQ4MHEtNjYgMC0xMTMtNDd0LTQ3LTExM3EwLTY2IDQ3LTExM3QxMTMtNDdxNjYgMCAxMTMgNDd0NDcgMTEzcTAgNjYtNDcgMTEzdC0xMTMgNDdabTQwMC0xNjBxMCA2Ni00NyAxMTN0LTExMyA0N3EtMTEgMC0yOC0yLjV0LTI4LTUuNXEyNy0zMiA0MS41LTcxdDE0LjUtODFxMC00Mi0xNC41LTgxVDU0NC03OTJxMTQtNSAyOC02LjV0MjgtMS41cTY2IDAgMTEzIDQ3dDQ3IDExM1pNMTIwLTI0MGg0ODB2LTMycTAtMTEtNS41LTIwVDU4MC0zMDZxLTU0LTI3LTEwOS00MC41VDM2MC0zNjBxLTU2IDAtMTExIDEzLjVUMTQwLTMwNnEtOSA1LTE0LjUgMTR0LTUuNSAyMHYzMlptMjQwLTMyMHEzMyAwIDU2LjUtMjMuNVQ0NDAtNjQwcTAtMzMtMjMuNS01Ni41VDM2MC03MjBxLTMzIDAtNTYuNSAyMy41VDI4MC02NDBxMCAzMyAyMy41IDU2LjVUMzYwLTU2MFptMCAzMjBabTAtNDAwWiIvPjwvc3ZnPg==';
	Editor.syncImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgNFYxTDggNWw0IDRWNmMzLjMxIDAgNiAyLjY5IDYgNiAwIDEuMDEtLjI1IDEuOTctLjcgMi44bDEuNDYgMS40NkMxOS41NCAxNS4wMyAyMCAxMy41NyAyMCAxMmMwLTQuNDItMy41OC04LTgtOHptMCAxNGMtMy4zMSAwLTYtMi42OS02LTYgMC0xLjAxLjI1LTEuOTcuNy0yLjhMNS4yNCA3Ljc0QzQuNDYgOC45NyA0IDEwLjQzIDQgMTJjMCA0LjQyIDMuNTggOCA4IDh2M2w0LTQtNC00djN6Ii8+PC9zdmc+';
	Editor.cloudImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iIzAwMDAwMCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTEyIDZjMi42MiAwIDQuODggMS44NiA1LjM5IDQuNDNsLjMgMS41IDEuNTMuMTFjMS41Ni4xIDIuNzggMS40MSAyLjc4IDIuOTYgMCAxLjY1LTEuMzUgMy0zIDNINmMtMi4yMSAwLTQtMS43OS00LTQgMC0yLjA1IDEuNTMtMy43NiAzLjU2LTMuOTdsMS4wNy0uMTEuNS0uOTVDOC4wOCA3LjE0IDkuOTQgNiAxMiA2bTAtMkM5LjExIDQgNi42IDUuNjQgNS4zNSA4LjA0IDIuMzQgOC4zNiAwIDEwLjkxIDAgMTRjMCAzLjMxIDIuNjkgNiA2IDZoMTNjMi43NiAwIDUtMi4yNCA1LTUgMC0yLjY0LTIuMDUtNC43OC00LjY1LTQuOTZDMTguNjcgNi41OSAxNS42NCA0IDEyIDR6Ii8+PC9zdmc+';
	Editor.cloudOffImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iIzAwMDAwMCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTI0IDE1YzAtMi42NC0yLjA1LTQuNzgtNC42NS00Ljk2QzE4LjY3IDYuNTkgMTUuNjQgNCAxMiA0Yy0xLjMzIDAtMi41Ny4zNi0zLjY1Ljk3bDEuNDkgMS40OUMxMC41MSA2LjE3IDExLjIzIDYgMTIgNmMzLjA0IDAgNS41IDIuNDYgNS41IDUuNXYuNUgxOWMxLjY2IDAgMyAxLjM0IDMgMyAwIC45OS0uNDggMS44NS0xLjIxIDIuNGwxLjQxIDEuNDFjMS4wOS0uOTIgMS44LTIuMjcgMS44LTMuODF6TTQuNDEgMy44NkwzIDUuMjdsMi43NyAyLjc3aC0uNDJDMi4zNCA4LjM2IDAgMTAuOTEgMCAxNGMwIDMuMzEgMi42OSA2IDYgNmgxMS43M2wyIDIgMS40MS0xLjQxTDQuNDEgMy44NnpNNiAxOGMtMi4yMSAwLTQtMS43OS00LTRzMS43OS00IDQtNGgxLjczbDggOEg2eiIvPjwvc3ZnPg==';
	Editor.calendarImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI0IDI0IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0cHgiIGZpbGw9IiMwMDAwMDAiPjxnPjxwYXRoIGQ9Ik0wLDBoMjR2MjRIMFYweiIgZmlsbD0ibm9uZSIvPjwvZz48Zz48cGF0aCBkPSJNMjAsNEg0QzIuOSw0LDIsNC45LDIsNnYxMmMwLDEuMSwwLjksMiwyLDJoMTZjMS4xLDAsMi0wLjksMi0yVjZDMjIsNC45LDIxLjEsNCwyMCw0eiBNOCwxMUg0VjZoNFYxMXogTTE0LDExaC00VjZoNFYxMXogTTIwLDExaC00VjZoNFYxMXogTTgsMThINHYtNWg0VjE4eiBNMTQsMThoLTR2LTVoNFYxOHogTTIwLDE4aC00di01aDRWMTh6Ii8+PC9nPjwvc3ZnPg==';
 	Editor.syncProblemImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMyAxMmMwIDIuMjEuOTEgNC4yIDIuMzYgNS42NEwzIDIwaDZ2LTZsLTIuMjQgMi4yNEM1LjY4IDE1LjE1IDUgMTMuNjYgNSAxMmMwLTIuNjEgMS42Ny00LjgzIDQtNS42NVY0LjI2QzUuNTUgNS4xNSAzIDguMjcgMyAxMnptOCA1aDJ2LTJoLTJ2MnpNMjEgNGgtNnY2bDIuMjQtMi4yNEMxOC4zMiA4Ljg1IDE5IDEwLjM0IDE5IDEyYzAgMi42MS0xLjY3IDQuODMtNCA1LjY1djIuMDljMy40NS0uODkgNi00LjAxIDYtNy43NCAwLTIuMjEtLjkxLTQuMi0yLjM2LTUuNjRMMjEgNHptLTEwIDloMlY3aC0ydjZ6Ii8+PC9zdmc+';
	Editor.tailSpin = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9Ii0yIC0yIDQ0IDQ0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPGRlZnM+CiAgICAgICAgPGxpbmVhckdyYWRpZW50IHgxPSI4LjA0MiUiIHkxPSIwJSIgeDI9IjY1LjY4MiUiIHkyPSIyMy44NjUlIiBpZD0iYSI+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiM4MDgwODAiIHN0b3Atb3BhY2l0eT0iMCIgb2Zmc2V0PSIwJSIvPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjODA4MDgwIiBzdG9wLW9wYWNpdHk9Ii42MzEiIG9mZnNldD0iNjMuMTQ2JSIvPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjODA4MDgwIiBvZmZzZXQ9IjEwMCUiLz4KICAgICAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPC9kZWZzPgogICAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxIDEpIj4KICAgICAgICAgICAgPHBhdGggZD0iTTM2IDE4YzAtOS45NC04LjA2LTE4LTE4LTE4IiBzdHJva2U9InVybCgjYSkiIHN0cm9rZS13aWR0aD0iNiI+CiAgICAgICAgICAgICAgICA8YW5pbWF0ZVRyYW5zZm9ybQogICAgICAgICAgICAgICAgICAgIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIKICAgICAgICAgICAgICAgICAgICB0eXBlPSJyb3RhdGUiCiAgICAgICAgICAgICAgICAgICAgZnJvbT0iMCAxOCAxOCIKICAgICAgICAgICAgICAgICAgICB0bz0iMzYwIDE4IDE4IgogICAgICAgICAgICAgICAgICAgIGR1cj0iMC45cyIKICAgICAgICAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICAgICAgICAgICAgPC9wYXRoPgogICAgICAgICAgICA8Y2lyY2xlIGZpbGw9IiM4MDgwODAiIGN4PSIzNiIgY3k9IjE4IiByPSIxIj4KICAgICAgICAgICAgICAgIDxhbmltYXRlVHJhbnNmb3JtCiAgICAgICAgICAgICAgICAgICAgYXR0cmlidXRlTmFtZT0idHJhbnNmb3JtIgogICAgICAgICAgICAgICAgICAgIHR5cGU9InJvdGF0ZSIKICAgICAgICAgICAgICAgICAgICBmcm9tPSIwIDE4IDE4IgogICAgICAgICAgICAgICAgICAgIHRvPSIzNjAgMTggMTgiCiAgICAgICAgICAgICAgICAgICAgZHVyPSIwLjlzIgogICAgICAgICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogICAgICAgICAgICA8L2NpcmNsZT4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=';
	Editor.mailImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iIzAwMDAwMCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTIyIDZjMC0xLjEtLjktMi0yLTJINGMtMS4xIDAtMiAuOS0yIDJ2MTJjMCAxLjEuOSAyIDIgMmgxNmMxLjEgMCAyLS45IDItMlY2em0tMiAwbC04IDQuOTlMNCA2aDE2em0wIDEySDRWOGw4IDUgOC01djEweiIvPjwvc3ZnPg==';
	Editor.cameraImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMThweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMThweCIgZmlsbD0iIzAwMDAwMCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE0LjEyIDRsMS44MyAySDIwdjEySDRWNmg0LjA1bDEuODMtMmg0LjI0TTE1IDJIOUw3LjE3IDRINGMtMS4xIDAtMiAuOS0yIDJ2MTJjMCAxLjEuOSAyIDIgMmgxNmMxLjEgMCAyLS45IDItMlY2YzAtMS4xLS45LTItMi0yaC0zLjE3TDE1IDJ6bS0zIDdjMS42NSAwIDMgMS4zNSAzIDNzLTEuMzUgMy0zIDMtMy0xLjM1LTMtMyAxLjM1LTMgMy0zbTAtMmMtMi43NiAwLTUgMi4yNC01IDVzMi4yNCA1IDUgNSA1LTIuMjQgNS01LTIuMjQtNS01LTV6Ii8+PC9zdmc+';
	Editor.tagsImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNODU2LTM5MCA1NzAtMTA0cS0xMiAxMi0yNyAxOHQtMzAgNnEtMTUgMC0zMC02dC0yNy0xOEwxMDMtNDU3cS0xMS0xMS0xNy0yNS41VDgwLTUxM3YtMjg3cTAtMzMgMjMuNS01Ni41VDE2MC04ODBoMjg3cTE2IDAgMzEgNi41dDI2IDE3LjVsMzUyIDM1M3ExMiAxMiAxNy41IDI3dDUuNSAzMHEwIDE1LTUuNSAyOS41VDg1Ni0zOTBaTTUxMy0xNjBsMjg2LTI4Ni0zNTMtMzU0SDE2MHYyODZsMzUzIDM1NFpNMjYwLTY0MHEyNSAwIDQyLjUtMTcuNVQzMjAtNzAwcTAtMjUtMTcuNS00Mi41VDI2MC03NjBxLTI1IDAtNDIuNSAxNy41VDIwMC03MDBxMCAyNSAxNy41IDQyLjVUMjYwLTY0MFptMjIwIDE2MFoiLz48L3N2Zz4=';
	Editor.contrastImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDgwLTgwcS04MyAwLTE1Ni0zMS41VDE5Ny0xOTdxLTU0LTU0LTg1LjUtMTI3VDgwLTQ4MHEwLTgzIDMxLjUtMTU2VDE5Ny03NjNxNTQtNTQgMTI3LTg1LjVUNDgwLTg4MHE4MyAwIDE1NiAzMS41VDc2My03NjNxNTQgNTQgODUuNSAxMjdUODgwLTQ4MHEwIDgzLTMxLjUgMTU2VDc2My0xOTdxLTU0IDU0LTEyNyA4NS41VDQ4MC04MFptNDAtODNxMTE5LTE1IDE5OS41LTEwNC41VDgwMC00ODBxMC0xMjMtODAuNS0yMTIuNVQ1MjAtNzk3djYzNFoiLz48L3N2Zz4=';
	Editor.chevronUpImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDgwLTUyOCAyOTYtMzQ0bC01Ni01NiAyNDAtMjQwIDI0MCAyNDAtNTYgNTYtMTg0LTE4NFoiLz48L3N2Zz4=';
	Editor.chevronDownImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDgwLTM0NCAyNDAtNTg0bDU2LTU2IDE4NCAxODQgMTg0LTE4NCA1NiA1Ni0yNDAgMjQwWiIvPjwvc3ZnPg==';
	Editor.chevronLeftImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNTYwLTI0MCAzMjAtNDgwbDI0MC0yNDAgNTYgNTYtMTg0IDE4NCAxODQgMTg0LTU2IDU2WiIvPjwvc3ZnPg==';
	Editor.chevronRightImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNTA0LTQ4MCAzMjAtNjY0bDU2LTU2IDI0MCAyNDAtMjQwIDI0MC01Ni01NiAxODQtMTg0WiIvPjwvc3ZnPg==';
	Editor.rightPanelOpenImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDYwLTMyMHYtMzIwTDMwMC00ODBsMTYwIDE2MFpNMjAwLTEyMHEtMzMgMC01Ni41LTIzLjVUMTIwLTIwMHYtNTYwcTAtMzMgMjMuNS01Ni41VDIwMC04NDBoNTYwcTMzIDAgNTYuNSAyMy41VDg0MC03NjB2NTYwcTAgMzMtMjMuNSA1Ni41VDc2MC0xMjBIMjAwWm00NDAtODBoMTIwdi01NjBINjQwdjU2MFptLTgwIDB2LTU2MEgyMDB2NTYwaDM2MFptODAgMGgxMjAtMTIwWiIvPjwvc3ZnPg==';
	Editor.rightPanelCloseImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNMzAwLTY0MHYzMjBsMTYwLTE2MC0xNjAtMTYwWk0yMDAtMTIwcS0zMyAwLTU2LjUtMjMuNVQxMjAtMjAwdi01NjBxMC0zMyAyMy41LTU2LjVUMjAwLTg0MGg1NjBxMzMgMCA1Ni41IDIzLjVUODQwLTc2MHY1NjBxMCAzMy0yMy41IDU2LjVUNzYwLTEyMEgyMDBabTQ0MC04MGgxMjB2LTU2MEg2NDB2NTYwWm0tODAgMHYtNTYwSDIwMHY1NjBoMzYwWm04MCAwaDEyMC0xMjBaIi8+PC9zdmc+';
	Editor.spinImage = 'data:image/gif;base64,R0lGODlhDAAMAPUxAEVriVp7lmCAmmGBm2OCnGmHn3OPpneSqYKbr4OcsIScsI2kto6kt46lt5KnuZmtvpquvpuvv56ywaCzwqK1xKu7yay9yq+/zLHAzbfF0bjG0bzJ1LzK1MDN18jT28nT3M3X3tHa4dTc49Xd5Njf5dng5t3k6d/l6uDm6uru8e7x8/Dz9fT29/b4+Pj5+fj5+vr6+v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkKADEAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAADAAMAAAGR8CYcEgsOgYAIax4CCQuQldrCBEsiK8VS2hoFGOrlJDA+cZQwkLnqyoJFZKviSS0ICrE0ec0jDAwIiUeGyBFGhMPFBkhZo1BACH5BAkKAC4ALAAAAAAMAAwAhVB0kFR3k1V4k2CAmmWEnW6Lo3KOpXeSqH2XrIOcsISdsImhtIqhtJCmuJGnuZuwv52wwJ+ywZ+ywqm6yLHBzbLCzrXEz7fF0LnH0rrI0r7L1b/M1sXR2cfT28rV3czW3s/Z4Nfe5Nvi6ODm6uLn6+Ln7OLo7OXq7efs7+zw8u/y9PDy9PX3+Pr7+////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZDQJdwSCxGDAIAoVFkFBwYSyIwGE4OkCJxIdG6WkJEx8sSKj7elfBB0a5SQg1EQ0SVVMPKhDM6iUIkRR4ZFxsgJl6JQQAh+QQJCgAxACwAAAAADAAMAIVGa4lcfZdjgpxkg51nhp5ui6N3kqh5lKqFnbGHn7KIoLOQp7iRp7mSqLmTqbqarr6br7+fssGitcOitcSuvsuuv8uwwMyzw861xNC5x9K6x9K/zNbDztjE0NnG0drJ1NzQ2eDS2+LT2+LV3ePZ4Oba4ebb4ufc4+jm6+7t8PLt8PPt8fPx8/Xx9PX09vf19/j3+Pn///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQ8CYcEgsUhQFggFSjCQmnE1jcBhqGBXiIuAQSi7FGEIgfIzCFoCXFCZiPO0hKBMiwl7ET6eUYqlWLkUnISImKC1xbUEAIfkECQoAMgAsAAAAAAwADACFTnKPT3KPVHaTYoKcb4yjcY6leZSpf5mtgZuvh5+yiqG0i6K1jqW3kae5nrHBnrLBn7LCoLPCobTDqbrIqrvIs8LOtMPPtcPPtcTPuMbRucfSvcrUvsvVwMzWxdHaydTcytXdzNbezdff0drh2ODl2+Ln3eTp4Obq4ujs5Ont5uvu6O3w6u7w6u7x7/L09vj5+vr7+vv7////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkdAmXBILHIcicOCUqxELKKPxKAYgiYd4oMAEWo8RVmjIMScwhmBcJMKXwLCECmMGAhPI1QRwBiaSixCMDFhLSorLi8wYYxCQQAh+QQJCgAxACwAAAAADAAMAIVZepVggJphgZtnhp5vjKN2kah3kqmBmq+KobSLorWNpLaRp7mWq7ybr7+gs8KitcSktsWnuManucexwM2ywc63xtG6yNO9ytS+ytW/zNbDz9jH0tvL1d3N197S2+LU3OPU3ePV3eTX3+Xa4efb4ufd5Onl6u7r7vHs7/Lt8PLw8/Xy9Pby9fb09ff2+Pn3+Pn6+vr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGSMCYcEgseiwSR+RS7GA4JFGF8RiWNiEiJTERgkjFGAQh/KTCGoJwpApnBkITKrwoCFWnFlEhaAxXLC9CBwAGRS4wQgELYY1CQQAh+QQJCgAzACwAAAAADAAMAIVMcI5SdZFhgZtti6JwjaR4k6mAma6Cm6+KobSLorWLo7WNo7aPpredsMCescGitMOitcSmuMaqu8ixwc2zws63xdC4xtG5x9K9ytXAzdfCztjF0NnF0drK1d3M1t7P2N/P2eDT2+LX3+Xe5Onh5+vi5+vj6Ozk6e3n7O/o7O/q7vHs7/Lt8PPu8fPx8/X3+Pn6+vv7+/v8/Pz///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRcCZcEgsmkIbTOZTLIlGqZNnchm2SCgiJ6IRqljFmQUiXIVnoITQde4chC9Y+LEQxmTFRkFSNFAqDAMIRQoCAAEEDmeLQQAh+QQJCgAwACwAAAAADAAMAIVXeZRefplff5lhgZtph59yjqV2kaeAmq6FnbGFnrGLorWNpLaQp7mRqLmYrb2essGgs8Klt8apusitvcquv8u2xNC7yNO8ydS8ytTAzdfBzdfM1t7N197Q2eDU3OPX3+XZ4ObZ4ebc4+jf5erg5erg5uvp7fDu8fPv8vTz9fb09vf19/j3+Pn4+fn5+vr6+/v///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRUCYcEgspkwjEKhUVJ1QsBNp0xm2VixiSOMRvlxFGAcTJook5eEHIhQcwpWIkAFQECkNy9AQWFwyEAkPRQ4FAwQIE2llQQAh+QQJCgAvACwAAAAADAAMAIVNcY5SdZFigptph6BvjKN0kKd8lquAmq+EnbGGn7KHn7ONpLaOpbearr+csMCdscCescGhtMOnuMauvsuzws60w862xdC9ytW/y9a/zNbCztjG0drH0tvK1N3M1t7N19/U3ePb4uff5urj6Ozk6e3l6u7m6u7o7PDq7vDt8PPv8vTw8vTw8/X19vf6+vv///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQ8CXcEgsvlytVUplJLJIpSEDUESFTELBwSgCCQEV42kjDFiMo4uQsDB2MkLHoEHUTD7DRAHC8VAiZ0QSCgYIDxhNiUEAOw==';
	Editor.svgSpinImage = 'data:image/svg+xml;base64,77u/PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMjAwIj4KICAgIDxzdHlsZT4KICAgICAgICBjaXJjbGUgewogICAgICAgICAgICBmaWxsOiBsaWdodC1kYXJrKCMzMzMzMzMsICNkYWRhZGEpOwogICAgICAgICAgICBzdHJva2U6IGxpZ2h0LWRhcmsoIzMzMzMzMywgI2RhZGFkYSk7CiAgICAgICAgfQogICAgPC9zdHlsZT4KICAgIDxjaXJjbGUgc3Ryb2tlLXdpZHRoPSIxMiIgcj0iMjAiIGN4PSIzMCIgY3k9IjEwMCI+CiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ib3BhY2l0eSIgY2FsY01vZGU9InNwbGluZSIgZHVyPSIyIiB2YWx1ZXM9IjE7MDsxOyIga2V5U3BsaW5lcz0iLjUgMCAuNSAxOy41IDAgLjUgMSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGJlZ2luPSItLjQiPjwvYW5pbWF0ZT4KICAgIDwvY2lyY2xlPgogICAgPGNpcmNsZSBzdHJva2Utd2lkdGg9IjEyIiByPSIyMCIgY3g9IjEwMCIgY3k9IjEwMCI+CiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ib3BhY2l0eSIgY2FsY01vZGU9InNwbGluZSIgZHVyPSIyIiB2YWx1ZXM9IjE7MDsxOyIga2V5U3BsaW5lcz0iLjUgMCAuNSAxOy41IDAgLjUgMSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGJlZ2luPSItLjIiPjwvYW5pbWF0ZT4KICAgIDwvY2lyY2xlPgogICAgPGNpcmNsZSBzdHJva2Utd2lkdGg9IjEyIiByPSIyMCIgY3g9IjE3MCIgY3k9IjEwMCI+CiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ib3BhY2l0eSIgY2FsY01vZGU9InNwbGluZSIgZHVyPSIyIiB2YWx1ZXM9IjE7MDsxOyIga2V5U3BsaW5lcz0iLjUgMCAuNSAxOy41IDAgLjUgMSIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGJlZ2luPSIwIj48L2FuaW1hdGU+CiAgICA8L2NpcmNsZT4KPC9zdmc+';
	Editor.errorImage = 'data:image/gif;base64,R0lGODlhEAAQAPcAAADGAIQAAISEhP8AAP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEAAAAALAAAAAAQABAAAAhoAAEIFBigYMGBCAkGGMCQ4cGECxtKHBAAYUQCEzFSHLiQgMeGHjEGEAAg4oCQJz86LCkxpEqHAkwyRClxpEyXGmGaREmTIsmOL1GO/DkzI0yOE2sKIMlRJsWhCQHENDiUaVSpS5cmDAgAOw==';
	Editor.smallPlusImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDdCMTdENjVCOEM4MTFFNDlCRjVBNDdCODU5NjNBNUMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDdCMTdENjZCOEM4MTFFNDlCRjVBNDdCODU5NjNBNUMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowN0IxN0Q2M0I4QzgxMUU0OUJGNUE0N0I4NTk2M0E1QyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowN0IxN0Q2NEI4QzgxMUU0OUJGNUE0N0I4NTk2M0E1QyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtjrjmgAAAAtSURBVHjaYvz//z8DMigvLwcLdHZ2MiKLMzEQCaivkLGsrOw/dU0cAr4GCDAARQsQbTFrv10AAAAASUVORK5CYII=';
	Editor.hiResImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAA+CAMAAACLMWy1AAAAh1BMVEUAAABMTExERERBQUFBQUFFRUVAQEBCQkJAQEA6OjpDQ0NKSkpBQUFBQUFERERERERBQUFCQkJCQkJCQkJJSUlBQUFCQkJDQ0NDQ0NCQkJDQ0NBQUFBQUFCQkJBQUFCQkJCQkJDQ0NCQkJHR0dBQUFCQkJCQkJAQEBCQkJDQ0NAQEBERERCQkIk1hS2AAAAKnRSTlMAAjj96BL7PgQFRwfu3TYazKuVjRXl1V1DPCn1uLGjnWNVIgy9hU40eGqPkM38AAACG0lEQVRYw+2X63KbMBCFzwZblgGDceN74muatpLe//m6MHV3gHGFAv2RjM94MAbxzdnVsQbBDKwH8AH8MDAyafzjqYeyOG04XE7RS8nIRDXg6BlT+rA0nmtAPh+NQRDxIASIMG44rAMrGunBgHwy3uUldxggIStGKp2f+DQc2O4h4eQsX3O2IFB/oEbsjOKbStnjAEA+zJ0ylZTbgvoDn8xNyn6Dbj5Kd4GsNpABa6duQPfSdEj88TgMAhKuCWjAkgmFXPLnsD0pWd3OFGdrMugQII/eOMPEiGOzqPMIeWrcSoMCg71W1pXBPvCP+gS/OdXqQ3uW23+93XGWLl/OaBb805bNcBPoEIcVJsnHzcxpZH86u5KZ9gDby5dQCcnKqdbke4ItI4Tzd7IW9hZQt4EO6GG9b9sYuuK9Wwn8TIr2xKbF2+3Nhr+qxChJ/AI6pIfCu4z4Zowp4ZUNihz79vewzctnHDwTvQO/hCdFBzrUGDOPn2Y/F8YKT4oOATLvlhOznzmBSdFBJWtc58y7r+UVFOCQczy3wpN6pegDqHtsCPTGvH9JuTO0Dyg8icldYPk+RB6g8Aofj4m2EKBvtTmUPD9xDd1pPcSReV2U5iD/ik2yrngtvvqBfPzOvKiDTKTsCdoHZJ7pLLffgTwlJ5vJdtJV2/jiAYaLvLGhMAEDO5QcDg2M/jOw/8Zn+K3ZwJvHT7ZffgC/NvA3zcybTeIfE4EAAAAASUVORK5CYII=';
	Editor.loResImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAA+CAMAAACLMWy1AAAAS1BMVEVAQEAAAAA1NTVBQUFDQ0NDQ0NFRUVERERBQUFBQUFBQUFAQEBBQUFBQUFCQkJCQkJCQkJBQUFCQkJDQ0NDQ0NCQkJCQkJCQkJGRkb5/XqTAAAAGXRSTlP+AAWODlASCsesX+Lc2LyWe3pwa1tCPjohjSJfoAAAAI1JREFUWMPt1MkKhTAMRuG0anvneXr/J71nUypKcdqI/N8yhLMKMZE1CahnClDQzMPB44ED3EgeCubgDWnWQMHpwTtKwTe+UHD4sJ94wbUEHHFGhILlYDeSnsQeabeCgsPBgB0MOZZ9oGA5GJFiJSfUULAfjLjARrhCwX7wh2YCDwVbwZkUBKqFFJRN+wOcwSgR2sREcgAAAABJRU5ErkJggg==';
	Editor.blankImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';
	Editor.facebookImage = IMAGE_PATH + '/facebook.png';
	Editor.tweetImage = IMAGE_PATH + '/tweet.png';
	
	/**
	 * Broken image symbol for offline SVG.
	 */
	Editor.svgBrokenImage = Graph.createSvgImage(10, 10, '<rect x="0" y="0" width="10" height="10" stroke="#000" fill="transparent"/><path d="m 0 0 L 10 10 L 0 10 L 10 0" stroke="#000" fill="transparent"/>');

	/**
	 * Error image for not found images
	 */	
	Editor.configurationKey = '.configuration';
		
	/**
	 * Error image for not found images
	 */	
	Editor.settingsKey = '.drawio-config';
	
	/**
	 * Default value for custom libraries in mxSettings.
	 */
	Editor.defaultCustomLibraries = [];
	
	/**
	 * Default value for custom libraries in mxSettings.
	 */
	Editor.enableCustomLibraries = true;
	
	/**
	 * Not yet implemented. Reading uncompressed supported.
	 */
	Editor.enableUncompressedLibraries = false;
	
	/**
	 * Specifies if custom properties should be enabled.
	 */
	Editor.enableCustomProperties = true;
		
	/**
	 * Specifies if the simple theme should be enabled. This theme can be used
	 * at runtime in the kennedy theme.
	 */
	Editor.enableSimpleTheme = true;
			
	/**
	 * Specifies if the URL should be rewritten to contain the selected page.
	 * Default is true for online app without embed.
	 */
	Editor.enableHashObjects = !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
		urlParams['embed'] != '1' && window.top == window.self;
	
	/**
	 * Sets the default value for including a copy of the diagram.
	 * Default is true.
	 */
	Editor.defaultIncludeDiagram = true;

	/**
	 * Specifies if custom properties should be enabled.
	 */
	Editor.enableServiceWorker = urlParams['pwa'] != '0' &&
		'serviceWorker' in navigator && (urlParams['offline'] == '1' ||
		urlParams['enableSW'] == '1' ||
		/.*\.diagrams\.net$/.test(window.location.hostname) ||
		/.*\.draw\.io$/.test(window.location.hostname));

	// Checks access to service worker in sandboxed context
	try
	{
		Editor.enableServiceWorker && navigator.serviceWorker;
	}
	catch (e)
	{
		Editor.enableServiceWorker = false;
	}

	/**
	 * Specifies if web fonts are enabled.
	 */
	Editor.enableWebFonts = !window.mxIsElectron;

	/**
	 * Specifies if local font scanning is enabled.
	 */
	Editor.enableLocalFonts = false;

	/**
	 * Holds the list of detected local font names.
	 */
	Editor.localFonts = null;

	/**
	 * Disables the shadow option in the format panel.
	 */
	Editor.enableShadowOption = !mxClient.IS_SF;

	/**
	 * Disables the export URL function.
	 */
	Editor.enableExportUrl = true;

	/**
	 * Disables fast real time collaboration while keeping slower real time collaboration enabled.
	 */
	Editor.enableRealtime = true;

	/**
	 * Enables cache for patches and Pusher for messages. Default is true.
	 */
	Editor.enableRealtimeCache = true;
	
	/**
	 * Enables P2P instead of Pusher for messages. (Ignored if enableRealtimeCache is false.)
	 * Default is false.
	 */
	Editor.p2pSyncNotify = false;
	
	/**
	 * Specifies if XML files should be compressed. Default is true.
	 */
	Editor.compressXml = true;

	/**
	 * Specifies if XML files should be compressed by default. Default is false.
	 */
	Editor.defaultCompressed = false;
	
	/**
	 * Specifies if XML files should be compressed. Default is true.
	 */
	Editor.oneDriveInlinePicker = (window.urlParams != null && window.urlParams['inlinePicker'] == '0') ? false : true;

	/**
	 * Specifies global variables.
	 */
	Editor.globalVars = null;

	/**
	 * Default border for image export (to allow for sketch style).
	 */
	Editor.defaultBorder = 5;

	/**
	 * Specifies if cell metadata should be added to SVG output. Default is false.
	 */
	Editor.addSvgMetadata = false;

	/**
	 * Specifies animations should be enabled. Default is true.
	 */
	Editor.enableAnimations = true;

	/**
	 * Specifies if window docking should be enabled. Default is true.
	 */
	Editor.enableWindowDocking = urlParams['embedInline'] != '1';

	/**
	 * Specifies paste should be at the mouse pointer location. Default is true.
	 */
	Editor.pasteAtMousePointer = true;

	/**
	 * Specifies if the diagram should be fit to the window on load. Default is true.
	 */
	Editor.fitDiagramOnLoad = true;

	/**
	 * Specifies if the diagram should be fit to the page on load. Default is true.
	 */
	Editor.fitDiagramOnPage = true;

	/**
	 * Specifies if link icons should be shown on shapes. Default is false.
	 */
	Editor.showLinkIcons = false;

	/**
	 * Specifies if tooltip icons should be shown on shapes. Default is false.
	 */
	Editor.showTooltipIcons = false;

	/**
	 * Specifies if fill patterns should be expanded to inline geometry for
	 * print and PDF export to avoid rasterization. Default is true.
	 */
	Editor.expandPatternsForPrint = true;

	/**
	 * Specifies the default text style.
	 */
	Editor.defaultTextStyle = 'text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;' +
		'align=center;verticalAlign=middle;rounded=0;';
	
	/**
	 * Specifies the default style for sticky notes.
	 */
	Editor.defaultNoteStyle = 'shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;' +
		'fontColor=#000000;darkOpacity=0.05;fillColor=#FFF9B2;strokeColor=none;fillStyle=solid;' +
		'direction=west;gradientDirection=north;gradientColor=#FFF2A1;shadow=1;size=20;autosizeText=1;'
	
	/**
	 * Specifies if ChatGPT should be enabled. Default is true only
	 * on app.diagrams.net (including test and preprod).
	 */
	Editor.enableAi = (/test\.draw\.io$/.test(window.location.hostname)) ||
		(/preprod\.diagrams\.net$/.test(window.location.hostname)) ||
		(/app\.diagrams\.net$/.test(window.location.hostname));
	
	/**
	 * Available AI configurations.
	 */
	Editor.aiActions = ['createPublic', 'create', 'update', 'assist'];

	/**
	 * Specifies the ChatGPT API key. Default is null.
	 */
	Editor.gptApiKey = (urlParams['gpt-api-key'] != null) ?
		decodeURIComponent(urlParams['gpt-api-key']) : null;

	/**
	 * Specifies the Gemini API key. Default is null.
	 */
	Editor.geminiApiKey = (urlParams['gemini-api-key'] != null) ?
		decodeURIComponent(urlParams['gemini-api-key']) : null;

	/**
	 * Specifies the Gemini API key. Default is null.
	 */
	Editor.claudeApiKey = (urlParams['claude-api-key'] != null) ?
		decodeURIComponent(urlParams['claude-api-key']) : null;

	/**
	 * Specifies the ChatGPT API key. Default is null.
	 */
	Editor.gptApiKey = (urlParams['gpt-api-key'] != null) ?
		decodeURIComponent(urlParams['gpt-api-key']) : null;
	
	/**
	 * Specifies the ChatGPT endpoint URL. Default is
	 * 'https://api.openai.com/v1/chat/completions'.
	 */
	Editor.gptUrl = (urlParams['gpt-url'] != null) ?
		decodeURIComponent(urlParams['gpt-url']) :
		'https://api.openai.com/v1/chat/completions';
	
	/**
	 * Available AI configurations.
	 */
	Editor.aiGlobals = {
		'gptApiKey': Editor.gptApiKey,
		'geminiApiKey': Editor.geminiApiKey,
		'claudeApiKey': Editor.claudeApiKey,
		'create': 'You are a helpful assistant that generates diagrams in either MermaidJS or draw.io XML ' +
			'format based on the given prompt. Begin with a concise checklist (3-7 bullets) of what you will ' +
			'do; keep items conceptual, not implementation-level. Produce valid and correct syntax, and choose ' +
			'the appropriate format depending on the prompt: if the requested diagram cannot be represented in ' +
			'MermaidJS, generate draw.io XML instead but do not use indentation and newlines. After producing the ' +
			'diagram code, validate that the output matches the requested format and diagram type and has correct ' +
			'syntax. Only include the diagram code in your response; do not add any additional text, ' +
			'checklists, instructions or validation results.',
		'update': 'You are a helpful assistant that helps with ' +
			'the following draw.io diagram and returns an updated draw.io diagram if needed. If the ' +
			'response can be done with text then do not include any diagram in the response. Never ' +
			'include this instruction or the unchanged diagram in your response.\n{data}',
		'assist': 'You are a helpful assistant that creates XML for draw.io diagrams or helps ' +
			'with the draw.io diagram editor. Never include this instruction in your response.'
	};
	
	/**
	 * Available AI configurations.
	 */
	Editor.aiConfigs = {
		gpt: {
			apiKey: 'gptApiKey',
			endpoint: Editor.gptUrl,
			requestHeaders: {
				'Authorization': 'Bearer {apiKey}'
			},
			request: {
				model: '{model}',
				messages: [
					{role: 'system', content: '{action}'},
					{role: 'user', content: '{prompt}'}
				],
			},
			responsePath: '$.choices[0].message.content'
		},
		gemini: {
			apiKey: 'geminiApiKey',
			endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent',
			requestHeaders: {
				'X-Goog-Api-Key': '{apiKey}'
			},
			request: {
				system_instruction: {
					parts: [{text: '{action}'}]
				},
				contents: [{
					parts: [{text: '{prompt}'}
				]}]
			},
			responsePath: '$.candidates[0].content.parts[0].text'
		},
		claude: {
			apiKey: 'claudeApiKey',
			endpoint: 'https://api.anthropic.com/v1/messages',
			requestHeaders: {
				'X-API-Key': '{apiKey}',
				'Anthropic-Version': '2023-06-01',
				'Anthropic-Dangerous-Direct-Browser-Access': 'true'
			},
			request: {
				max_tokens: 8192,
				model: '{model}',
				messages: [
					{role: 'assistant', content: '{action}'},
					{role: 'user', content: '{prompt}'}
				],
			},
			responsePath: '$.content[0].text'
		}
	};

	/**
	 * Adds a list of available AI models.
	 */
	Editor.aiModels = [
		{name: 'Claude 4.6 Opus', model: 'claude-opus-4-6', config: 'claude'},
		{name: 'Claude 4.6 Sonnet', model: 'claude-sonnet-4-6', config: 'claude'},
		{name: 'Gemini 3 Pro Preview', model: 'gemini-3-pro-preview', config: 'gemini'},
		{name: 'Gemini 2.5 Pro', model: 'gemini-2.5-pro', config: 'gemini'},
		{name: 'GPT-5.1', model: 'gpt-5.1-2025-11-13', config: 'gpt'},
		{name: 'GPT-4.1', model: 'gpt-4.1-2025-04-14', config: 'gpt'},
	];

	/**
	 * Specifies if data URIs should be replaced with SVG sub-trees in SVG export.
	 * Default is true.
	 */
	Editor.replaceSvgDataUris = true;
	
	/**
	 * Specifies if foreignObject alternate content should be replaced with an image
	 * of the HTML text. Default is true.
	 */
	Editor.foreignObjectImages = true;
		
	/**
	 * Specifies the theme to use for SVG files. Possible values are 'light',
	 * 'dark' and 'auto'. Default is 'auto'.
	 */
	Editor.svgFileTheme = 'auto';
	
	/**
	 * Specifies the scale used to rasterize SVG images. Default is 4.
	 */
	Editor.svgRasterScale = 4;
			
	/**
	 * Specifies the scale used to rasterize HTML markup. Default is 4.
	 */
	Editor.htmlRasterScale = 4;
	
	/**
	 * Reference to the config object passed to <configure>.
	 */
	Editor.config = null;

	/**
	 * Reference to the version of the last config object in
	 * <configure>. If this is different to the last version in
	 * mxSettings.parse, then the settings are reset.
	 */
	Editor.configVersion = null;

	/**
	 * Reference to the version of the last config object in
	 * <configure>. If this is different to the last version in
	 * mxSettings.parse, then the settings are reset.
	 */
	Editor.createInheritProperty = function(title, key)
	{
		return {
			name: key, dispName: title, type: 'bool', defVal: false, isVisible: function(state, format)
			{
				if (state.vertices.length == 1)
				{
					var model = format.editorUi.editor.graph.model;
					var parent = model.getParent(state.vertices[0]);

					return model.isEdge(parent) || model.isVertex(parent);
				}
				else
				{
					return false;
				}
			}, getValue: function(state, format)
			{
				return mxUtils.getValue(state.style, key) == 'inherit';
			}, valueChanged: function(newVal, input, state, format)
			{
				var graph = format.editorUi.editor.graph;

				if (newVal == '1')
				{
					graph.setCellStyles(key, 'inherit', state.vertices);
				}
				else
				{
					graph.setCellStyles(key, null, state.vertices);
				}
			}
		};
	};

	/**
	 * Common properties for all edges.
	 */
	Editor.commonProperties = [
		{name: 'enumerate', dispName: 'Enumerate', type: 'bool', defVal: false, onChange: function(graph)
		{
			graph.refresh();
		}},
		{name: 'enumerateValue', dispName: 'Enumerate Value', type: 'string', defVal: '', isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, 'enumerate', '0') == '1';
		}},
        {name: 'comic', dispName: 'Comic', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', '0') != '1';
        }},
        {name: 'jiggle', dispName: 'Jiggle', type: 'float', min: 0, defVal: 1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'comic', '0') == '1' ||
        		mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'fillWeight', dispName: 'Fill Weight', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'hachureGap', dispName: 'Hachure Gap', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'hachureAngle', dispName: 'Hachure Angle', type: 'int', defVal: -41, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'curveFitting', dispName: 'Curve Fitting', type: 'float', defVal: 0.95, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'simplification', dispName: 'Simplification', type: 'float', defVal: 0, min: 0, max: 1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'disableMultiStroke', dispName: 'Disable Multi Stroke', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'disableMultiStrokeFill', dispName: 'Disable Multi Stroke Fill', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'dashOffset', dispName: 'Dash Offset', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'dashGap', dispName: 'Dash Gap', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'zigzagOffset', dispName: 'ZigZag Offset', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'sketchStyle', dispName: 'Sketch Style', type: 'enum', defVal: 'rough',
        	enumList: [{val: 'rough', dispName: 'Rough'}, {val: 'comic', dispName: 'Comic'}],
        	isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
		{name: mxConstants.STYLE_SHADOWCOLOR, dispName: 'Shadow Color', type: 'color', getDefaultValue: function()
		{
			return mxConstants.SHADOWCOLOR;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_OPACITY, dispName: 'Shadow Opacity', type: 'int', min: 0, max: 100, getDefaultValue: function()
		{
			return mxConstants.SHADOW_OPACITY * 100;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_OFFSET_X, dispName: 'Shadow Offset X', type: 'int', getDefaultValue: function()
		{
			return mxConstants.SHADOW_OFFSET_X;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_OFFSET_Y, dispName: 'Shadow Offset Y', type: 'int', getDefaultValue: function()
		{
			return mxConstants.SHADOW_OFFSET_Y;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_BLUR, dispName: 'Shadow Blur', type: 'int', min: 0, getDefaultValue: function()
		{
			return mxConstants.SHADOW_BLUR;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
        {name: 'linecap', dispName: 'Line Cap', type: 'enum', defVal: null,
        	enumList: [{val: null, dispName: 'Flat'}, {val: 'round', dispName: 'Round'}, {val: 'square', dispName: 'Square'}]
        },
		{name: 'linejoin', dispName: 'Line Join', type: 'enum', defVal: null,
        	enumList: [{val: null, dispName: 'Miter'}, {val: 'arcs', dispName: 'Arcs'}, {val: 'bevel', dispName: 'Bevel'},
			{val: 'miter-clip', dispName: 'Miter-Clip'}, {val: 'round', dispName: 'Round'}]
        },
		{name: 'miterlimit', dispName: 'Miter Limit', type: 'int', min: 1, defVal: 4, isVisible: function(state, format)
		{
			var linejoin = mxUtils.getValue(state.style, 'linejoin', 'miter');

			return linejoin == 'miter' || linejoin == 'miter-clip';
		}}
	];
	
	/**
	 * Common properties for all edges.
	 */
	Editor.commonEdgeProperties = [
        {type: 'separator'},
        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
        {name: 'bezier', dispName: 'Bezier', type: 'bool', defVal: false, isVisible: function(state)
        {
    		return mxUtils.getValue(state.style, 'curved', '0') == '1';
        }},
        {name: 'sourcePortConstraint', dispName: 'Source Constraint', type: 'enum', defVal: 'none',
        	enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'targetPortConstraint', dispName: 'Target Constraint', type: 'enum', defVal: 'none',
        	enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'jettySize', dispName: 'Jetty Size', type: 'int', min: 0, defVal: 'auto', allowAuto: true, isVisible: function(state)
        {
    		return mxUtils.getValue(state.style, mxConstants.STYLE_EDGE, null) == 'orthogonalEdgeStyle';
        }},
        {name: 'segment', dispName: 'Segment Size', type: 'int', min: 0, defVal: mxConstants.ENTITY_SEGMENT, isVisible: function(state)
        {
    		return mxUtils.getValue(state.style, mxConstants.STYLE_EDGE, null) == 'entityRelationEdgeStyle';
        }},
        {name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'startFill', dispName: 'Start Fill', type: 'bool', defVal: true},
		{name: 'startFillColor', dispName: 'Start Fill Color', type: 'color', defVal: null},
        {name: 'endFill', dispName: 'End Fill', type: 'bool', defVal: true},
		{name: 'endFillColor', dispName: 'End Fill Color', type: 'color', defVal: null},
        {name: 'perimeterSpacing', dispName: 'Terminal Spacing', type: 'float', defVal: 0},
        {name: 'anchorPointDirection', dispName: 'Anchor Direction', type: 'bool', defVal: true},
        {name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
        {name: 'dashPattern', dispName: 'Dash Pattern', type: 'numbers', defVal: ''},
        {name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
		{name: 'flowAnimationDuration', dispName: 'Flow Duration', type: 'int', defVal: 500, isVisible: function(state)
		{
			return mxUtils.getValue(state.style, 'flowAnimation', null) == '1';
		}},
		{name: 'flowAnimationTimingFunction', dispName: 'Flow Timing', type: 'enum', defVal: 'linear',
			enumList: [{val: 'linear', dispName: 'Linear'}, {val: 'ease', dispName: 'Ease'}, {val: 'ease-in', dispName: 'Ease-in'},
			{val: 'ease-out', dispName: 'Ease-out'}, {val: 'ease-in-out', dispName: 'Ease-in-out'}], isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'flowAnimation', null) == '1';
			}
		},
		{name: 'flowAnimationDirection', dispName: 'Flow Direction', type: 'enum', defVal: 'normal',
			enumList: [{val: 'normal', dispName: 'Normal'}, {val: 'reverse', dispName: 'Reverse'},
			{val: 'alternate', dispName: 'Alternate'}, {val: 'alternate-reverse', dispName: 'Alternate-Reverse'}],
			isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'flowAnimation', null) == '1';
			}
		},
		{name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
        {name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
        {name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
        {name: 'bendable', dispName: 'Bendable', type: 'bool', defVal: true},
        {name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
        {name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
        {name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
        {name: 'noJump', dispName: 'No Jumps', type: 'bool', defVal: false},
		{name: 'ignoreEdge', dispName: 'Ignore Edge', type: 'bool', defVal: false},
        {name: 'orthogonalLoop', dispName: 'Loop Routing', type: 'bool', defVal: false},
		{name: 'orthogonal', dispName: 'Orthogonal', type: 'bool', defVal: false}
	].concat(Editor.commonProperties);

	/**
	 * Common properties for all vertices.
	 */
	Editor.commonVertexProperties = [
        {name: 'colspan', dispName: 'Colspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
        }},
        {name: 'rowspan', dispName: 'Rowspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
        }},
        {type: 'separator'},
        {name: 'resizeLastRow', dispName: 'Resize Last Row', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	var style = graph.getCellStyle(cell);
        	
        	return mxUtils.getValue(style, 'resizeLastRow', '0') == '1';
        }, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 &&
    			graph.isTable(state.vertices[0]);
        }},
        {name: 'resizeLast', dispName: 'Resize Last Column', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	var style = graph.getCellStyle(cell);
        	
        	return mxUtils.getValue(style, 'resizeLast', '0') == '1';
        }, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 &&
    			graph.isTable(state.vertices[0]);
        }},
        {name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'overflow', dispName: 'Text Overflow', defVal: 'visible', type: 'enum',
        	enumList: [{val: 'visible', dispName: 'Visible'}, {val: 'hidden', dispName: 'Hidden'}, {val: 'block', dispName: 'Block'},
        		{val: 'fill', dispName: 'Fill'}, {val: 'width', dispName: 'Width'}]
        },
        {name: 'noLabel', dispName: 'Hide Label', type: 'bool', defVal: false},
        {name: 'labelPadding', dispName: 'Label Padding', type: 'float', defVal: 0},
        {name: 'direction', dispName: 'Direction', type: 'enum', defVal: 'east',
        	enumList: [{val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'portConstraint', dispName: 'Constraint', type: 'enum', defVal: 'none',
        	enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'portConstraintRotation', dispName: 'Rotate Constraint', type: 'bool', defVal: false},
        {name: 'connectable', dispName: 'Connectable', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length > 0 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	
        	return graph.isCellConnectable(cell);
        }, isVisible: function(state, format)
        {
    		return state.vertices.length > 0 && state.edges.length == 0;
        }},
        {name: 'allowArrows', dispName: 'Allow Arrows', type: 'bool', defVal: true},
        {name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
        {name: 'perimeter', dispName: 'Perimeter', defVal: 'none', type: 'enum',
        	enumList: [{val: 'none', dispName: 'None'},
        			{val: 'rectanglePerimeter', dispName: 'Rectangle'}, {val: 'ellipsePerimeter', dispName: 'Ellipse'},
        			{val: 'rhombusPerimeter', dispName: 'Rhombus'}, {val: 'trianglePerimeter', dispName: 'Triangle'},
        			{val: 'hexagonPerimeter2', dispName: 'Hexagon'}, {val: 'lifelinePerimeter', dispName: 'Lifeline'},
        			{val: 'orthogonalPerimeter', dispName: 'Orthogonal'}, {val: 'backbonePerimeter', dispName: 'Backbone'},
        			{val: 'calloutPerimeter', dispName: 'Callout'}, {val: 'parallelogramPerimeter', dispName: 'Parallelogram'},
        			{val: 'trapezoidPerimeter', dispName: 'Trapezoid'}, {val: 'stepPerimeter', dispName: 'Step'},
        			{val: 'centerPerimeter', dispName: 'Center'}]
        },
        {name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
        {name: 'container', dispName: 'Container', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length > 0 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;

			return cell != null && graph.isSwimlane(cell);
		}, isVisible: function(state, format)
        {
    		return state.vertices.length > 0 && state.edges.length == 0;
        }},
        {name: 'dropTarget', dispName: 'Drop Target', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	
        	return cell != null && (graph.isSwimlane(cell) || graph.model.getChildCount(cell) > 0);
        }, isVisible: function(state, format)
        {
    		return state.vertices.length == 1 && state.edges.length == 0;
        }},
        {name: 'collapsible', dispName: 'Collapsible', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	
        	return cell != null && ((graph.isContainer(cell) && state.style['collapsible'] != '0') ||
        		(!graph.isContainer(cell) && state.style['collapsible'] == '1'));
        }, isVisible: function(state, format)
        {
    		return state.vertices.length == 1 && state.edges.length == 0;
        }},
        {name: 'recursiveResize', dispName: 'Resize Children', type: 'bool', defVal: true, isVisible: function(state, format)
        {
    		return state.vertices.length == 1 && state.edges.length == 0 &&
    			!format.editorUi.editor.graph.isSwimlane(state.vertices[0]) &&
    			mxUtils.getValue(state.style, 'childLayout', null) == null;
        }},
        {name: 'expand', dispName: 'Expand', type: 'bool', defVal: true},
		{name: 'contract', dispName: 'Contract', type: 'bool', defVal: false},
		{name: 'groupPadding', dispName: 'Group Padding', type: 'int', defVal: 0, min: 0},
        {name: 'part', dispName: 'Part', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	var model = format.editorUi.editor.graph.model;
        	
        	return (state.vertices.length > 0) ? model.isVertex(model.getParent(state.vertices[0])) : false;
        }},
        {name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
        {name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
        {name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
        {name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
        {name: 'movableLabel', dispName: 'Movable Label', type: 'bool', defVal: false, isVisible: function(state, format)
        {
    		var geo = (state.vertices.length > 0) ? format.editorUi.editor.graph.getCellGeometry(state.vertices[0]) : null;
    		
    		return geo != null && !geo.relative;
        }},
        {name: 'autosize', dispName: 'Autosize', type: 'bool', defVal: false},
		{name: 'autosizeGrid', dispName: 'Autosize Grid', type: 'enum', defVal: null,
        	enumList: [{val: null, dispName: 'Default'}, {val: '1', dispName: 'Enabled'}, {val: '0', dispName: 'Disabled'}], isVisible: function(state, format)
			{
				return state.vertices.length > 0 && format.editorUi.editor.graph.isAutoSizeCell(state.vertices[0]);
			}
        },
        {name: 'fixedWidth', dispName: 'Fixed Width', type: 'bool', defVal: false},
        {name: 'resizable', dispName: 'Resizable', type: 'bool', defVal: true},
        {name: 'resizeWidth', dispName: 'Resize Width', type: 'bool', defVal: false},
        {name: 'resizeHeight', dispName: 'Resize Height', type: 'bool', defVal: false},
        {name: 'rotatable', dispName: 'Rotatable', type: 'bool', defVal: true},
        {name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
        {name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
        {name: 'treeFolding', dispName: 'Tree Folding', type: 'bool', defVal: false},
        {name: 'treeMoving', dispName: 'Tree Moving', type: 'bool', defVal: false},
        {name: 'pointerEvents', dispName: 'Pointer Events', type: 'bool', defVal: true, isVisible: function(state, format)
        {
        	var fillColor = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, null);
        	
        	return !mxShape.forceFilledPointerEvents ||
				(format.editorUi.editor.graph.isSwimlane(state.vertices[0]) ||
        		fillColor == null || fillColor == mxConstants.NONE ||
				mxUtils.getValue(state.style, mxConstants.STYLE_FILL_OPACITY, 100) == 0 ||
				mxUtils.getValue(state.style, mxConstants.STYLE_OPACITY, 100) == 0 ||
				state.style['pointerEvents'] != null);
        }},
        {name: 'moveCells', dispName: 'Move Cells on Fold', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return state.vertices.length > 0 && format.editorUi.editor.graph.isContainer(state.vertices[0]);
        }},
		{name: 'expandToFront', dispName: 'Expand to Front', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return state.vertices.length > 0 && format.editorUi.editor.graph.isContainer(state.vertices[0]);
        }},
		Editor.createInheritProperty('Inherit Font Family', mxConstants.STYLE_FONTFAMILY),
		Editor.createInheritProperty('Inherit Font Size', mxConstants.STYLE_FONTSIZE),
		Editor.createInheritProperty('Inherit Font Color', mxConstants.STYLE_FONTCOLOR),
		Editor.createInheritProperty('Inherit Fill Color', mxConstants.STYLE_FILLCOLOR),
		Editor.createInheritProperty('Inherit Stroke Color', mxConstants.STYLE_STROKECOLOR)
	].concat(Editor.commonProperties);

	/**
	 * Default value for the CSV import dialog.
	 */
	Editor.defaultCsvValue = '##\n' +
		'## Example CSV import. Use ## for comments and # for configuration. Paste CSV below.\n' +
		'## The following names are reserved and should not be used (or ignored):\n' +
		'## id, tooltip, placeholder(s), link and label (see below)\n' +
		'##\n' +
		'#\n' +
		'## Node label with placeholders and HTML.\n' +
		'## Default is \'%name_of_first_column%\'.\n' +
		'#\n' +
		'# label: %name%<br><i style="color:gray;">%position%</i><br><a href="mailto:%email%">Email</a>\n' +
		'#\n' +
		'## Node style (placeholders are replaced once).\n' +
		'## Default is the current style for nodes.\n' +
		'#\n' +
		'# style: label;image=%image%;whiteSpace=wrap;html=1;rounded=1;fillColor=%fill%;strokeColor=%stroke%;\n' +
		'#\n' +
		'## Parent style for nodes with child nodes (placeholders are replaced once).\n' +
		'#\n' +
		'# parentstyle: swimlane;whiteSpace=wrap;html=1;childLayout=stackLayout;horizontal=1;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;\n' +
		'#\n' +
		'## Style to be used for objects not in the CSV. If this is - then such objects are ignored,\n' +
		'## else they are created using this as their style, eg. whiteSpace=wrap;html=1;\n' +
		'#\n' +
		'# unknownStyle: -\n' +
		'#\n' +
		'## Optional column name that contains a reference to a named style in styles.\n' +
		'## Default is the current style for nodes.\n' +
		'#\n' +
		'# stylename: -\n' +
		'#\n' +
		'## JSON for named styles of the form {"name": "style", "name": "style"} where style is a cell style with\n' +
		'## placeholders that are replaced once.\n' +
		'#\n' +
		'# styles: -\n' +
		'#\n' +
		'## JSON for variables in styles of the form {"name": "value", "name": "value"} where name is a string\n' +
		'## that will replace a placeholder in a style.\n' +
		'#\n' +
		'# vars: -\n' +
		'#\n' +
		'## Optional column name that contains a reference to a named label in labels.\n' +
		'## Default is the current label.\n' +
		'#\n' +
		'# labelname: -\n' +
		'#\n' +
		'## JSON for named labels of the form {"name": "label", "name": "label"} where label is a cell label with\n' +
		'## placeholders.\n' +
		'#\n' +
		'# labels: -\n' +
		'#\n' +
		'## Uses the given column name as the identity for cells (updates existing cells).\n' +
		'## Default is no identity (empty value or -).\n' +
		'#\n' +
		'# identity: -\n' +
		'#\n' +
		'## Uses the given column name as the parent reference for cells. Default is no parent (empty or -).\n' +
		'## The identity above is used for resolving the reference so it must be specified.\n' +
		'#\n' +
		'# parent: -\n' +
		'#\n' +
		'## Adds a prefix to the identity of cells to make sure they do not collide with existing cells (whose\n' +
		'## IDs are numbers from 0..n, sometimes with a GUID prefix in the context of realtime collaboration).\n' +
		'## Default is csvimport-.\n' +
		'#\n' +
		'# namespace: csvimport-\n' +
		'#\n' +
		'## Connections between rows ("from": source colum, "to": target column).\n' +
		'## Label, style and invert are optional. Defaults are \'\', current style and false.\n' +
		'## If placeholders are used in the style, they are replaced with data from the source.\n' +
		'## An optional placeholders can be set to target to use data from the target instead.\n' +
		'## In addition to label, an optional fromlabel and tolabel can be used to name the column\n' +
		'## that contains the text for the label in the edges source or target (invert ignored).\n' +
		'## In addition to those, an optional source and targetlabel can be used to specify a label\n' +
		'## that contains placeholders referencing the respective columns in the source or target row.\n' +
		'## The label is created in the form fromlabel + sourcelabel + label + tolabel + targetlabel.\n' +
		'## Additional labels can be added by using an optional labels array with entries of the\n' +
		'## form {"label": string, "x": number, "y": number, "dx": number, "dy": number} where\n' +
		'## x is from -1 to 1 along the edge, y is orthogonal, and dx/dy are offsets in pixels.\n' +
		'## An optional placeholders with the string value "source" or "target" can be specified\n' +
		'## to replace placeholders in the additional label with data from the source or target.\n' +
		'## An optional data object can be specified to define the metadata for the connector.\n' +
		'## The target column may contain a comma-separated list of values.\n' +
		'## Multiple connect entries are allowed.\n' +
		'#\n' +
		'# connect: {"from": "manager", "to": "name", "invert": true, "label": "manages", \\\n' +
		'#          "style": "curved=1;endArrow=blockThin;endFill=1;fontSize=11;"}\n' +
		'# connect: {"from": "refs", "to": "id", "style": "curved=1;fontSize=11;"}\n' +
		'#\n' +
		'## Node x-coordinate. Possible value is a column name. Default is empty. Layouts will\n' +
		'## override this value.\n' +
		'#\n' +
		'# left: \n' +
		'#\n' +
		'## Node y-coordinate. Possible value is a column name. Default is empty. Layouts will\n' +
		'## override this value.\n' +
		'#\n' +
		'# top: \n' +
		'#\n' +
		'## Node width. Possible value is a number (in px), auto or an @ sign followed by a column\n' +
		'## name that contains the value for the width. Default is auto.\n' +
		'#\n' +
		'# width: auto\n' +
		'#\n' +
		'## Node height. Possible value is a number (in px), auto, width or an @ sign followed by a column\n' +
		'## name that contains the value for the height. Default is auto.\n' +
		'#\n' +
		'# height: auto\n' +
		'#\n' +
		'## Collapsed state for vertices. Possible values are true or false. Default is false.\n' +
		'#\n' +
		'# collapsed: false\n' +
		'#\n' +
		'## Padding for autosize. Default is 0.\n' +
		'#\n' +
		'# padding: -12\n' +
		'#\n' +
		'## Comma-separated list of ignored columns for metadata. (These can be\n' +
		'## used for connections and styles but will not be added as metadata.)\n' +
		'#\n' +
		'# ignore: id,image,fill,stroke,refs,manager\n' +
		'#\n' +
		'## Column to be renamed to link attribute (used as link).\n' +
		'#\n' +
		'# link: url\n' +
		'#\n' +
		'## Spacing between nodes. Default is 40.\n' +
		'#\n' +
		'# nodespacing: 40\n' +
		'#\n' +
		'## Spacing between levels of hierarchical layouts. Default is 100.\n' +
		'#\n' +
		'# levelspacing: 100\n' +
		'#\n' +
		'## Spacing between parallel edges. Default is 40. Use 0 to disable.\n' +
		'#\n' +
		'# edgespacing: 40\n' +
		'#\n' +
		'## Name or JSON of layout. Possible values are auto, none, verticaltree, horizontaltree,\n' +
		'## verticalflow, horizontalflow, organic, circle, orgchart or a JSON string as used in\n' +
		'## Layout, Apply. Default is auto.\n' +
		'#\n' +
		'# layout: auto\n' +
		'#\n' +
		'## ---- CSV below this line. First line are column names. ----\n' +
		'name,position,id,location,manager,email,fill,stroke,refs,url,image\n' +
		'Tessa Miller,CFO,emi,Office 1,,me@example.com,default,#6c8ebf,,https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-3-128.png\n' +
		'Edward Morrison,Brand Manager,emo,Office 2,Tessa Miller,me@example.com,default,#82b366,,https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-10-3-128.png\n' +
		'Alison Donovan,System Admin,rdo,Office 3,Tessa Miller,me@example.com,default,#82b366,"emo,tva",https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-2-128.png\n' +
		'Evan Valet,HR Director,tva,Office 4,Tessa Miller,me@example.com,default,#82b366,,https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-9-2-128.png\n';

	/**
	 * Capability check for canvas API
	 */
	Editor.canvasSupported = false;
	
	(function()
	{
		try
		{
			var cnv = document.createElement('canvas');
			Editor.canvasSupported = !!(cnv.getContext && cnv.getContext('2d'));
		}
		catch (e)
		{
			// ignore
		}
	})();
	
	/**
	 * Capability check for canvas export
	 */
	Editor.useCanvasForExport = false;

	(function()
	{
		try
		{
			var canvas = document.createElement('canvas');
			var img = new Image();
			
			// LATER: Capability check should not be async
			img.onload = function()
			{
				try
				{
					var ctx = canvas.getContext('2d');
					ctx.drawImage(img, 0, 0);

					// Works in Chrome, Firefox, Edge, Safari and Opera
					var result = canvas.toDataURL('image/png');
					Editor.useCanvasForExport = result != null && result.length > 6;
				}
				catch (e)
				{
					// ignore
				}
			};

			// Checks if SVG with foreignObject can be exported
			var svg = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1px" height="1px" version="1.1"><foreignObject pointer-events="all" width="1" height="1"><div xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>';
			img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
		}
		catch (e)
		{
			// ignore
		}
	})();

	/**
	 * Capability check for canvas export formats
	 */
	Editor.jpgSupported = false;
	Editor.webpSupported = false;
	
	(function()
	{
		// Checks for client-side JPG and WebP support
		try
		{
		    var canvas = document.createElement('canvas');
		    canvas.width = canvas.height = 1;
			
		    var uri = canvas.toDataURL('image/jpeg');
		    Editor.jpgSupported = (uri.match('image/jpeg') !== null);

			uri = canvas.toDataURL('image/webp');
		    Editor.webpSupported = (uri.match('image/webp') !== null);
		}
		catch (e)
		{
			// ignore
		}
	})();

	/**
	 * Replaces light-dark color functions with light colors for older browsers.
	 * See https://caniuse.com/mdn-css_types_color_light-dark
	 * TODO: Remove this in January 2027.
	 */
	Editor.loadCompatibleCss = function(cssFile)
	{
		if (!mxUtils.lightDarkColorSupported)
		{
			cssFile = cssFile || 'styles/grapheditor.css';

			function replaceLightDarkFunctions(cssString)
			{
				var compatCss = cssString.replace(/light-dark\(\s*(var\([^\)]+\)|[^,]+)\s*,\s*(var\([^\)]+\)|[^)]+)\s*\)/g, '$1');

				// Replace :has with :not and adds additional work arounds
				// see https://caniuse.com/css-has
				try
				{
					if (!CSS.supports('selector(:has(*))'))
					{
						compatCss = compatCss.replace(/:has\(/g, ':not(') + '\n' +
							'.geFormatTitleContainer { overflow: hidden !important; }\n' +
							'.geRuler+.geRuler { margin-top: 14px !important; }\n' +
							'.geDiagramContainer { margin: 0 !important; }';
					}
				}
				catch (e)
				{
					// Ignore errors in unsupported browsers
				}

				return compatCss;
			};

			mxUtils.get(cssFile, mxUtils.bind(this, function(req)
			{
				if (req.getStatus() >= 200 && req.getStatus() <= 299)
				{
					var style = document.createElement('style');
					style.type = 'text/css';
					style.appendChild(document.createTextNode(
						replaceLightDarkFunctions(req.getText())));
					document.head.appendChild(style);
				}
			}));
		}
	};
	
	/**
	 * Executes a simple JSONPath expression on the given object and
	 * returns the results as an array.
	 * 
	 * This is a highly limited function for basic dot/index paths only.
	 */
	Editor.replacePlaceholders = function(str, resolver)
	{
		var result = [];
		
		if (str != null && resolver != null)
		{
			var regex = new RegExp('\{([^\{\}"\'=;]+)\}', 'g');
			var match = null;
			var last = 0;

			// Since replacePlaceholders may be called recursively, we cannot use
			// a resuable regular expression here (as exec maintains state).
			while (match = regex.exec(str))
			{
				var val = match[0];

				if (val.length > 2)
				{
					var value = resolver(val.substring(1, val.length - 1));
					var tmp = val;

					if (value != null)
					{
						tmp = value;
					}
					
					result.push(str.substring(last, match.index) + tmp);
					last = match.index + val.length;
				}
			}
			
			result.push(str.substring(last));
		}

		return result.join('');
	};

	/**
	 * Executes a simple JSONPath expression on the given object and
	 * returns the results as an array.
	 * 
	 * This is a highly limited function for basic dot/index paths only.
	 */
	Editor.executeSimpleJsonPath = function(jsonObject, pathExpression)
	{
		// 1. Removes the root indicator '$' and split the path by '.'
		var steps = pathExpression.replace(/^\$\.?/, '').split('.'); 
		var current = jsonObject;

		for (var step of steps)
		{
			if (!current)
			{
				return [];
			}

			// Handle array index notation (e.g., 'key[0]')
			var arrayMatch = step.match(/([^\[]+)\[(\d+)\]/);
			
			if (arrayMatch)
			{
				var key = arrayMatch[1];
				var index = parseInt(arrayMatch[2]);
				current = current[key];

				if (Array.isArray(current) && index < current.length)
				{
					current = current[index];
				}
				else
				{
					return []; // Path invalid
				}
			}
			else
			{
				// Simple key access
				current = current[step];
			}
		}
		
		// Return result as an array for consistency with standard jsonpath
		return current !== undefined ? [current] : [];
	};

	/**
	 * Compresses the given string.
	 */
	Editor.createRoughCanvas = function(c)
	{
		var rc = rough.canvas(
		{
			// Provides expected function but return value is not used
			getContext: function()
			{
				return c;
			}
		});
		
		rc.draw = function(drawable)
		{
			var sets = drawable.sets || [];
			var o = drawable.options || this.getDefaultOptions();

			for (var i = 0; i < sets.length; i++)
			{
				var drawing = sets[i];
				
				switch (drawing.type)
				{
					case 'path':
						if (o.stroke != null)
						{
							this._drawToContext(c, drawing, o);
						}
						break;
					case 'fillPath':
						this._drawToContext(c, drawing, o);
						break;
					case 'fillSketch':
						this.fillSketch(c, drawing, o);
						break;
				}
			}
		};
	
		rc.fillSketch = function(ctx, drawing, o)
		{
			var strokeColor = c.state.strokeColor;
			var strokeWidth = c.state.strokeWidth;
			var strokeAlpha = c.state.strokeAlpha;
			var dashed = c.state.dashed;
			
			var fweight = o.fillWeight;
			if (fweight < 0)
			{
				fweight = o.strokeWidth / 2;
			}

			c.setStrokeAlpha(c.state.fillAlpha);
			c.setStrokeColor(o.fill || '');
			c.setStrokeWidth(fweight);
			c.setDashed(false);
			
			this._drawToContext(ctx, drawing, o);
			
			c.setDashed(dashed);
			c.setStrokeWidth(strokeWidth);
			c.setStrokeColor(strokeColor);
			c.setStrokeAlpha(strokeAlpha);
		};
	
		rc._drawToContext = function(ctx, drawing, o)
		{
			ctx.begin();
			
			for (var i = 0; i < drawing.ops.length; i++)
			{
				var item = drawing.ops[i];
				var data = item.data;
				
				switch (item.op)
				{
					case 'move':
						ctx.moveTo(data[0], data[1]);
						break;
					case 'bcurveTo':
						ctx.curveTo(data[0], data[1], data[2], data[3], data[4], data[5]);
						break;
					case 'lineTo':
						ctx.lineTo(data[0], data[1]);
						break;
				}
			};
	
			ctx.end();
	
			if (drawing.type === 'fillPath' && o.filled)
			{
				ctx.fill();
			}
			else
			{
				ctx.stroke();
			}
		};
	
		return rc;
	};
	
	/**
	 * Uses RoughJs for drawing comic shapes.
	 */
	(function()
	{
		/**
		 * Adds handJiggle style (jiggle=n sets jiggle)
		 */
		function RoughCanvas(canvas, rc, shape)
		{
			this.canvas = canvas;
			this.rc = rc;
			this.shape = shape;
			
			// Avoids "spikes" in the output
			this.canvas.setLineJoin('round');
			this.canvas.setLineCap('round');
			
			this.originalBegin = this.canvas.begin;
			this.canvas.begin = mxUtils.bind(this, RoughCanvas.prototype.begin);
			
			this.originalEnd = this.canvas.end;
			this.canvas.end = mxUtils.bind(this, RoughCanvas.prototype.end);
					
			this.originalRect = this.canvas.rect;
			this.canvas.rect = mxUtils.bind(this, RoughCanvas.prototype.rect);
	
			this.originalRoundrect = this.canvas.roundrect;
			this.canvas.roundrect = mxUtils.bind(this, RoughCanvas.prototype.roundrect);
			
			this.originalEllipse = this.canvas.ellipse;
			this.canvas.ellipse = mxUtils.bind(this, RoughCanvas.prototype.ellipse);
			
			this.originalLineTo = this.canvas.lineTo;
			this.canvas.lineTo = mxUtils.bind(this, RoughCanvas.prototype.lineTo);
			
			this.originalMoveTo = this.canvas.moveTo;
			this.canvas.moveTo = mxUtils.bind(this, RoughCanvas.prototype.moveTo);
			
			this.originalQuadTo = this.canvas.quadTo;
			this.canvas.quadTo = mxUtils.bind(this, RoughCanvas.prototype.quadTo);
			
			this.originalCurveTo = this.canvas.curveTo;
			this.canvas.curveTo = mxUtils.bind(this, RoughCanvas.prototype.curveTo);
			
			this.originalArcTo = this.canvas.arcTo;
			this.canvas.arcTo = mxUtils.bind(this, RoughCanvas.prototype.arcTo);
			
			this.originalClose = this.canvas.close;
			this.canvas.close = mxUtils.bind(this, RoughCanvas.prototype.close);
			
			this.originalFill = this.canvas.fill;
			this.canvas.fill = mxUtils.bind(this, RoughCanvas.prototype.fill);
			
			this.originalStroke = this.canvas.stroke;
			this.canvas.stroke = mxUtils.bind(this, RoughCanvas.prototype.stroke);
			
			this.originalFillAndStroke = this.canvas.fillAndStroke;
			this.canvas.fillAndStroke = mxUtils.bind(this, RoughCanvas.prototype.fillAndStroke);
			
			this.path = [];
			this.passThrough = false;
		};
	
		RoughCanvas.prototype.moveOp = 'M';
		RoughCanvas.prototype.lineOp = 'L';
		RoughCanvas.prototype.quadOp = 'Q';
		RoughCanvas.prototype.curveOp = 'C';
		RoughCanvas.prototype.closeOp = 'Z';
	
		RoughCanvas.prototype.getStyle = function(stroke, fill)
		{
			// Random seed created from cell ID
			var seed = 1;

			if (this.shape.state != null)
			{
				var str = this.shape.state.cell.id;
				
				if (str != null)
				{
					for (var i = 0; i < str.length; i++)
					{
				    	seed = ((seed << 5) - seed + str.charCodeAt(i)) << 0;
					}
				}
			}

			var style = {strokeWidth: this.canvas.state.strokeWidth, seed: seed, preserveVertices: true};
			var defs = this.rc.getDefaultOptions();
			
			if (stroke)
			{
				style.stroke = this.canvas.state.strokeColor === mxConstants.NONE ?
					'transparent' : this.canvas.state.strokeColor;
			}
			else
			{
				style.stroke = mxConstants.NONE;
			}
			
			var gradient = null;
			style.filled = fill;
			
			if (fill)
			{
				style.fill = this.canvas.state.fillColor === mxConstants.NONE ?
					'' : this.canvas.state.fillColor;
				gradient = this.canvas.state.gradientColor === mxConstants.NONE ?
					null : this.canvas.state.gradientColor;
			}
			else
			{
				style.fill = '';
			}
			
			// Applies cell style
			style['bowing'] = mxUtils.getValue(this.shape.style, 'bowing', defs['bowing']);
			style['hachureAngle'] = mxUtils.getValue(this.shape.style, 'hachureAngle', defs['hachureAngle']);
			style['curveFitting'] = mxUtils.getValue(this.shape.style, 'curveFitting', defs['curveFitting']);
			style['roughness'] = mxUtils.getValue(this.shape.style, 'jiggle', defs['roughness']);
			style['simplification'] = mxUtils.getValue(this.shape.style, 'simplification', defs['simplification']);
			style['disableMultiStroke'] = mxUtils.getValue(this.shape.style, 'disableMultiStroke', defs['disableMultiStroke']);
			style['disableMultiStrokeFill'] = mxUtils.getValue(this.shape.style, 'disableMultiStrokeFill', defs['disableMultiStrokeFill']);
		
			var hachureGap = mxUtils.getValue(this.shape.style, 'hachureGap', -1);
			style['hachureGap'] = (hachureGap == 'auto') ? -1 : hachureGap;
			style['dashGap'] = mxUtils.getValue(this.shape.style, 'dashGap', hachureGap);
			style['dashOffset'] = mxUtils.getValue(this.shape.style, 'dashOffset', hachureGap);
			style['zigzagOffset'] = mxUtils.getValue(this.shape.style, 'zigzagOffset', hachureGap);
			
			var fillWeight = mxUtils.getValue(this.shape.style, 'fillWeight', -1);
			style['fillWeight'] = (fillWeight == 'auto') ? -1 : fillWeight;
			
			var fillStyle = mxUtils.getValue(this.shape.style, 'fillStyle', 'auto');
			
			// Dots fill style is disable due to performance problems
			if (fillStyle == 'dots')
			{
				fillStyle = 'auto';
			}
			
			if (fillStyle == 'auto')
			{
				// One of the following backgrounds for solid fill
				var bg = [mxUtils.hex2rgb('#ffffff')];
				
				if (this.shape.state != null)
				{
					bg.push(mxUtils.hex2rgb(this.shape.state.view.graph.shapeBackgroundColor));
				}

				if (Editor.isDarkMode())
				{
					bg.push(mxUtils.hex2rgb(Editor.darkColor));
				}

				fillStyle = (style.fill != null && (gradient != null || mxUtils.indexOf(
					bg, mxUtils.hex2rgb(style.fill)) >= 0)) ? 'solid' : defs['fillStyle'];
			}

			style['fillStyle'] = fillStyle;
			
			return style;
		};
		
		RoughCanvas.prototype.begin = function()
		{
			if (this.passThrough)
			{
				this.originalBegin.apply(this.canvas, arguments);
			}
			else
			{
				this.path = [];
			}
		};
		
		RoughCanvas.prototype.end = function()
		{
			if (this.passThrough)
			{
				this.originalEnd.apply(this.canvas, arguments);
			}
			else
			{
				// do nothing
			}
		};
		
		RoughCanvas.prototype.addOp = function()
		{
			if (this.path != null)
			{
				this.path.push(arguments[0]);
				
				if (arguments.length > 2)
				{
					var s = this.canvas.state;
		
					for (var i = 2; i < arguments.length; i += 2)
					{
						this.lastX = arguments[i - 1];
						this.lastY = arguments[i];
						
						this.path.push(this.canvas.format((this.lastX)));
						this.path.push(this.canvas.format((this.lastY)));
					}
				}
			}
		};
	
		RoughCanvas.prototype.lineTo = function(endX, endY)
		{
			if (this.passThrough)
			{
				this.originalLineTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.lineOp, endX, endY);
				this.lastX = endX;
				this.lastY = endY;
			}
		};
		
		RoughCanvas.prototype.moveTo = function(endX, endY)
		{
			if (this.passThrough)
			{
				this.originalMoveTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.moveOp, endX, endY);
				this.lastX = endX;
				this.lastY = endY;
				this.firstX = endX;
				this.firstY = endY;
			}
		};
		
		RoughCanvas.prototype.close = function()
		{
			if (this.passThrough)
			{
				this.originalClose.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.closeOp);
			}
		};
		
		RoughCanvas.prototype.quadTo = function(x1, y1, x2, y2)
		{
			if (this.passThrough)
			{
				this.originalQuadTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.quadOp, x1, y1, x2, y2);
				this.lastX = x2;
				this.lastY = y2;
			}
		};
		
		RoughCanvas.prototype.curveTo = function(x1, y1, x2, y2, x3, y3)
		{
			if (this.passThrough)
			{
				this.originalCurveTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.curveOp, x1, y1, x2, y2, x3, y3);
				this.lastX = x3;
				this.lastY = y3;
			}
		};
		
		RoughCanvas.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y)
		{
			if (this.passThrough)
			{
				this.originalArcTo.apply(this.canvas, arguments);
			}
			else
			{
				var curves = mxUtils.arcToCurves(this.lastX, this.lastY, rx, ry, angle, largeArcFlag, sweepFlag, x, y);
				
				if (curves != null)
				{
					for (var i = 0; i < curves.length; i += 6) 
					{
						this.curveTo(curves[i], curves[i + 1], curves[i + 2],
							curves[i + 3], curves[i + 4], curves[i + 5]);
					}
				}
				
				this.lastX = x;
				this.lastY = y;
			}
		};
			
		RoughCanvas.prototype.rect = function(x, y, w, h)
		{
			if (this.passThrough)
			{
				this.originalRect.apply(this.canvas, arguments);
			}
			else
			{
				this.path = [];
				this.nextShape = this.rc.generator.rectangle(x, y, w, h, this.getStyle(true, true));
			}
		};
	
		RoughCanvas.prototype.ellipse = function(x, y, w, h)
		{
			if (this.passThrough)
			{
				this.originalEllipse.apply(this.canvas, arguments);
			}
			else
			{
				this.path = [];
				this.nextShape = this.rc.generator.ellipse(x + w / 2, y + h / 2, w, h, this.getStyle(true, true));
			}
		};
			
		RoughCanvas.prototype.roundrect = function(x, y, w, h, dx, dy)
		{
			if (this.passThrough)
			{
				this.originalRoundrect.apply(this.canvas, arguments);
			}
			else
			{
				this.begin();
				this.moveTo(x + dx, y);
				this.lineTo(x + w - dx, y);
				this.quadTo(x + w, y, x + w, y + dy);
				this.lineTo(x + w, y + h - dy);
				this.quadTo(x + w, y + h, x + w - dx, y + h);
				this.lineTo(x + dx, y + h);
				this.quadTo(x, y + h, x, y + h - dy);
				this.lineTo(x, y + dy);
				this.quadTo(x, y, x + dx, y);
			}
		};
	
		RoughCanvas.prototype.drawPath = function(style)
		{
			if (this.path.length > 0)
			{
				this.passThrough = true;
				try
				{
					this.rc.path(this.path.join(' '), style);
				}
				catch (e)
				{
					// ignore
				}
				this.passThrough = false;
			}
			else if (this.nextShape != null)
			{
				for (var key in style)
				{
					this.nextShape.options[key] = style[key];
				}
				
				if (style['stroke'] == mxConstants.NONE ||
					style['stroke'] == null)
				{
					delete this.nextShape.options['stroke'];
				}
				
				if (!style.filled)
				{
					delete this.nextShape.options['fill'];
				}
	
				this.passThrough = true;
				this.rc.draw(this.nextShape);
				this.passThrough = false;
			}	
		};
		
		RoughCanvas.prototype.stroke = function()
		{
			if (this.passThrough)
			{
				this.originalStroke.apply(this.canvas, arguments);
			}
			else
			{
				this.drawPath(this.getStyle(true, false));
			}
		};
		
		RoughCanvas.prototype.fill = function()
		{
			if (this.passThrough)
			{
				this.originalFill.apply(this.canvas, arguments);
			}
			else
			{
				this.drawPath(this.getStyle(false, true));
			}
		};
		
		RoughCanvas.prototype.fillAndStroke = function()
		{
			if (this.passThrough)
			{
				this.originalFillAndStroke.apply(this.canvas, arguments);
			}
			else
			{
				this.drawPath(this.getStyle(true, true));
			}
		};
		
		RoughCanvas.prototype.destroy = function()
		{
			 this.canvas.lineTo = this.originalLineTo;
			 this.canvas.moveTo = this.originalMoveTo;
			 this.canvas.close = this.originalClose;
			 this.canvas.quadTo = this.originalQuadTo;
			 this.canvas.curveTo = this.originalCurveTo;
			 this.canvas.arcTo = this.originalArcTo;
			 this.canvas.close = this.originalClose;
			 this.canvas.fill = this.originalFill;
			 this.canvas.stroke = this.originalStroke;
			 this.canvas.fillAndStroke = this.originalFillAndStroke;
			 this.canvas.begin = this.originalBegin;
			 this.canvas.end = this.originalEnd;
			 this.canvas.rect = this.originalRect;
			 this.canvas.ellipse = this.originalEllipse;
			 this.canvas.roundrect = this.originalRoundrect;
		};
				
		// Returns a new HandJiggle canvas
		mxShape.prototype.createRoughCanvas = function(c)
		{
			return new RoughCanvas(c, Editor.createRoughCanvas(c), this);	
		};
			
		// Overrides to include sketch style
		var shapeCreateHandJiggle = mxShape.prototype.createHandJiggle;
		mxShape.prototype.createHandJiggle = function(c)
		{
			if (!this.outline && this.style != null &&
				mxUtils.getValue(this.style, 'sketch', '0') != '0')
			{
				if (mxUtils.getValue(this.style, 'sketchStyle', 'rough') == 'comic')
				{
					return this.createComicCanvas(c);
				}
				else
				{
					return this.createRoughCanvas(c);	
				}
			}
			else
			{
				return shapeCreateHandJiggle.apply(this, arguments);
			}
		};

		// Avoids duplicate painting of images
		var imageShapePaintVertexShape = mxImageShape.prototype.paintVertexShape;

		mxImageShape.prototype.paintVertexShape = function(c, x, y, w, h)
		{
			if (c.handJiggle == null || !c.handJiggle.passThrough)
			{
				imageShapePaintVertexShape.apply(this, arguments);
			}
		};

		// Overrides for event handling on transparent background for sketch style
		var shapePaint = mxShape.prototype.paint;
		mxShape.prototype.paint = function(c)
		{
			var addTolerance = c.addTolerance;
			var events = true;
			
			if (this.style != null)
			{
				events = mxUtils.getValue(this.style, mxConstants.STYLE_POINTER_EVENTS, '1') == '1';
			}
			
			if (c.handJiggle != null && c.handJiggle.constructor == RoughCanvas && !this.outline)
			{
				// Save needed for possible transforms applied during paint
				c.save();
				var fill = this.fill;
				var stroke = this.stroke;
				this.fill = null;
				this.stroke = null;
				
				var configurePointerEvents = this.configurePointerEvents;
				
				// Ignores color changes during paint
				var setStrokeColor = c.setStrokeColor;
				
				c.setStrokeColor = function()
				{
					// ignore
				};
		
				var setFillColor = c.setFillColor;
				
				c.setFillColor = function()
				{
					// ignore
				};
				
				// Adds stroke tolerance for plain rendering if filled
				if (!events && fill != null)
				{
					this.configurePointerEvents = function()
					{
						// ignore
					};
				}
				
				c.handJiggle.passThrough = true;

				shapePaint.apply(this, arguments);

				c.handJiggle.passThrough = false;
				c.setFillColor = setFillColor;
				c.setStrokeColor = setStrokeColor;
				this.configurePointerEvents = configurePointerEvents;
				this.stroke = stroke;
				this.fill = fill;
				c.restore();
				
				// Bypasses stroke tolerance for sketched rendering if filled
				if (events && fill != null)
				{
					c.addTolerance = function()
					{
						// ignore	
					};
				}
			}
			
			shapePaint.apply(this, arguments);
			c.addTolerance = addTolerance;
		};

		// Overrides glass effect to disable sketch style
		var shapePaintGlassEffect = mxShape.prototype.paintGlassEffect;
		mxShape.prototype.paintGlassEffect = function(c, x, y, w, h, arc)
		{
			if (c.handJiggle != null && c.handJiggle.constructor == RoughCanvas)
			{
				c.handJiggle.passThrough = true;
				shapePaintGlassEffect.apply(this, arguments);
				c.handJiggle.passThrough = false;
			}
			else
			{
				shapePaintGlassEffect.apply(this, arguments);
			}
		};
	})();

	/**
	 * Compresses the given string.
	 */
	Editor.fastCompress = function(data)
	{
		if (data == null || data.length == 0 || typeof(pako) === 'undefined')
		{
			return data;
		}
		else
		{
			return Graph.arrayBufferToString(pako.deflateRaw(data));
		}
	};

	/**
	 * Decompresses the given string.
	 */
	Editor.fastDecompress = function(data)
	{
	   	if (data == null || data.length == 0 || typeof(pako) === 'undefined')
		{
			return data;
		}
		else
		{
			return pako.inflateRaw(Graph.stringToArrayBuffer(atob(data)), {to: 'string'});
		}
	};

	/**
	 * Helper function to extract the graph model XML node.
	 */
	Editor.extractGraphModel = function(node, allowMxFile, checked)
	{
		if (node != null && typeof(pako) !== 'undefined')
		{
			var tmp = node.ownerDocument.getElementsByTagName('div');
			var divs = [];
			
			if (tmp != null && tmp.length > 0)
			{
				for (var i = 0; i < tmp.length; i++)
				{
					if (tmp[i].getAttribute('class') == 'mxgraph')
					{
						divs.push(tmp[i]);
						break;
					}	
				}
			}
			
			if (divs.length > 0)
			{
				var data = divs[0].getAttribute('data-mxgraph');

				if (data != null)
				{
					var config = JSON.parse(data);

					if (config != null && config.xml != null)
					{
						var doc2 = mxUtils.parseXml(config.xml);
						node = doc2.documentElement;
					}
				}
				else
				{
					var divs2 = divs[0].getElementsByTagName('div');
					
					if (divs2.length > 0)
					{
						var data = mxUtils.getTextContent(divs2[0]);
		        		data = Graph.decompress(data, null, checked);
		        		
		        		if (data.length > 0)
		        		{
		        			var doc2 = mxUtils.parseXml(data);
		        			node = doc2.documentElement;
		        		}
					}
				}
			}
		}
		
		if (node != null && node.nodeName == 'svg')
		{
			var tmp = node.getAttribute('content');
			
			if (tmp != null && tmp.charAt(0) != '<' && tmp.charAt(0) != '%')
			{
				tmp = unescape((window.atob) ? atob(tmp) : Base64.decode(cont, tmp));
			}
			
			if (tmp != null && tmp.charAt(0) == '%')
			{
				tmp = decodeURIComponent(tmp);
			}
			
			if (tmp != null && tmp.length > 0)
			{
				node = mxUtils.parseXml(tmp).documentElement;
			}
			else
			{
				throw {message: mxResources.get('notADiagramFile')};
			}
		}
		
		if (node != null && !allowMxFile)
		{
			var diagramNode = null;
			
			if (node.nodeName == 'diagram')
			{
				diagramNode = node;
			}
			else if (node.nodeName == 'mxfile')
			{
				var diagrams = node.getElementsByTagName('diagram');

				if (diagrams.length > 0)
				{
					diagramNode = diagrams[Math.max(0, Math.min(diagrams.length - 1, urlParams['page'] || 0))];
				}
			}
			
			if (diagramNode != null)
			{
				node = Editor.parseDiagramNode(diagramNode, checked);
			}
		}
		
		if (node != null && node.nodeName != 'mxGraphModel' && (!allowMxFile || node.nodeName != 'mxfile'))
		{
			node = null;
		}
		
		return node;
	};
	
	/**
	 * Extracts the XML from the compressed or non-compressed text chunk.
	 */
	Editor.parseDiagramNode = function(diagramNode, checked, allowRecurse)
	{
		Editor.validateDiagramNode(diagramNode);
		var text = mxUtils.trim(mxUtils.getTextContent(diagramNode));
		var node = null;
		
		if (text.length > 0)
		{
			var tmp = Graph.decompress(text, null, checked);
			
			if (tmp != null && tmp.length > 0)
			{
				node = mxUtils.parseXml(tmp).documentElement;
			}
		}
		else
		{
			var temp = mxUtils.getChildNodes(diagramNode);
			
			if (temp.length > 0)
			{
				var tempNode = temp[0];

				if (allowRecurse)
				{
					tempNode = Editor.parseDiagramNode(tempNode, checked, false);
				}

				// Creates new document for unique IDs within mxGraphModel
				var doc = mxUtils.createXmlDocument();
				doc.appendChild(doc.importNode(tempNode, true));
				node = doc.documentElement;
			}
		}
		
		return node;
	};
	
	/**
	 * Extracts the XML from the compressed or non-compressed text chunk.
	 */
	Editor.getDiagramNodeXml = function(diagramNode)
	{
		Editor.validateDiagramNode(diagramNode);
		var text = mxUtils.getNodeValue(diagramNode);
		var xml = null;
		
		if (text.length > 0)
		{
			xml = Graph.decompress(text);
		}
		else
		{
			var temp = diagramNode.getElementsByTagName('mxGraphModel');

			if (temp != null && temp.length > 0)
			{
				xml = mxUtils.getXml(temp[0]);
			}
		}
		
		return xml;
	};

	/**
	 * Initializes diagram nodes with no content.
	 */
	Editor.validateDiagramNode = function(diagramNode)
	{
		if (mxUtils.trim(mxUtils.getTextContent(diagramNode)).length == 0 &&
			mxUtils.getChildNodes(diagramNode).length == 0)
		{
			var codec = new mxCodec();
			var model = new mxGraphModel();
			diagramNode.appendChild(codec.encode(model));
		}

		return diagramNode;
	};

	/**
	 * Static method for parsing PDF files.
	 */
	Editor.extractGraphModelFromPdf = function(base64)
	{
		var result = null;
		base64 = base64.substring(base64.indexOf(',') + 1);

		// Workaround for invalid character error in Safari
		var f = (window.atob && !mxClient.IS_SF) ? atob(base64) : Base64.decode(base64, true);

		// Extracts Subject or Embedded file attachment from PDF 1.7
		if (f.substring(0, 8) == '%PDF-1.7')
		{
			var blockStart = f.indexOf('EmbeddedFile'); 
			
			if (blockStart > -1)
			{
				var streamStart = f.indexOf('stream', blockStart) + 9; //the start of the stream [skipping header check]
				var fileInfo = f.substring(blockStart, streamStart);
				
				if (fileInfo.indexOf('application#2Fvnd.jgraph.mxfile') > 0)
				{
					var streamEnd = f.indexOf('endstream', streamStart - 1);
				
					return pako.inflateRaw(Graph.stringToArrayBuffer(
						f.substring(streamStart, streamEnd)), {to: 'string'});
				}
			}

			var last = f.indexOf('/ObjStm');

			while (last > 0)
			{
				var streamStart = f.indexOf('stream', last) + 9; //the start of the stream [skipping header check]
				var streamEnd = f.indexOf('endstream', streamStart - 1);
				
				function hex_to_ascii(hex)
				{
					var str = [];
					
					for (var n = 0; n < hex.length; n += 2)
					{
						var code = hex.substr(n, 2);

						// Encoded mxfile is URI encoded ASCII
						if (code != '00')
						{
							str.push(String.fromCharCode(parseInt(code, 16)));
						}
					}

					return str.join('');
				};

				var text = pako.inflateRaw(Graph.stringToArrayBuffer(
					f.substring(streamStart, streamEnd)), {to: 'string'});
				var subj = text.indexOf('/Subject <');

				// Extracts Subject from PDF 1.4
				if (subj > 0)
				{
					var temp = text.substring(subj + 14, text.indexOf('>', subj));

					if (temp != null)
					{
						result = hex_to_ascii(temp);
					}

					break;
				}

				last = f.indexOf('/ObjStm', last + 1);
			}
		}

		// Extracts subject from PDF 1.4
		if (result == null && f.substring(0, 8) == '%PDF-1.4')
		{
			var check = '/Subject (%3Cmxfile';
			var curline = '';
			var checked = 0;
			var pos = 0;
			var obj = [];
			var buf = null;
			
			while (pos < f.length)
			{
				var b = f.charCodeAt(pos);
				pos += 1;
				
				if (b != 10)
				{
					curline += String.fromCharCode(b);
				}
				
				if (b == check.charCodeAt(checked))
				{
					checked++;
				}
				else
				{
					checked = 0;
				}
				
				if (checked == check.length)
				{
					var end = f.indexOf('%3C%2Fmxfile%3E', pos) + 15; //15 is the length of encoded </mxfile>
					pos -= 9; //9 is the length of encoded <mxfile

					// Default case is XML inlined in Subject metadata
					if (end > pos)
					{
						result = f.substring(pos, end);

						break;
					}
				}
				
				// Creates table for lookup if no inline data is found
				if (b == 10)
				{
					if (curline == 'endobj')
					{
						buf = null;
					}
					else if (curline.substring(curline.length - 3, curline.length) == 'obj' ||
						curline == 'xref' || curline == 'trailer')
					{
						buf = [];
						obj[curline.split(' ')[0]] = buf;
					}
					else if (buf != null)
					{
						buf.push(curline);
					}
					
					curline = '';
				}
			}
		}
		
		// Extract XML via references
		if (result == null && obj != null)
		{
			result = Editor.extractGraphModelFromXref(obj);
		}
		
		if (result != null)
		{
			result = decodeURIComponent(result.
				replace(/\\\(/g, "(").
				replace(/\\\)/g, ")"));
		}
		
		return result;
	};

	/**
	 * Static method for extracting Subject via references of the form
	 * 
	 * << /Size 33 /Root 20 0 R /Info 1 0 R and 1 0 obj << /Subject 22 0 R
	 * 
	 * Where Info is the metadata block and Subject is the data block.
	 */
	Editor.extractGraphModelFromXref = function(obj)
	{
		var trailer = obj['trailer'];
		var result = null;

		// Gets Info object
		if (trailer != null)
		{
			var arr = /.* \/Info (\d+) (\d+) R/g.exec(trailer.join('\n'));
			
			if (arr != null && arr.length > 0)
			{
				var info = obj[arr[1]];
				
				if (info != null)
				{
					arr = /.* \/Subject (\d+) (\d+) R/g.exec(info.join('\n'));
				
					if (arr != null && arr.length > 0)
					{
						var subj = obj[arr[1]];
						
						if (subj != null)
						{
							subj = subj.join('\n');
							result = subj.substring(1, subj.length - 1);
						}
					}
				}
			}
		}
		
		return result;
	};

	/**
	 * Extracts any parsers errors in the given XML.
	 */
	Editor.extractParserError = function(node, defaultCause)
	{
		var cause = null;
		var errors = (node != null) ? node.getElementsByTagName('parsererror') : null;
		
		if (errors != null && errors.length > 0)
		{
			cause = defaultCause || mxResources.get('invalidChars');
			var divs = errors[0].getElementsByTagName('div');
			
			if (divs.length > 0)
			{
				cause = mxUtils.getTextContent(divs[0]);
			}
		}
		
		return (cause != null) ? mxUtils.trim(cause) : cause;
	};
	
	/**
	 * Adds the given retry function to the given error.
	 */
	Editor.addRetryToError = function(err, retry)
	{
		if (err != null)
		{
			var e = (err.error != null) ? err.error : err;
			
			if (e.retry == null)
			{
				e.retry = retry;
			}
		}
	};
	
	/**
	 * 
	 * Hook for mermaid to draw.io converter.
	 */
	Editor.mermaidToDrawio = function(graph, diagramtype, exta)
	{
		if (typeof mxMermaidToDrawio === 'function')
		{
			return mxMermaidToDrawio(graph, diagramtype, exta);
		}
	};
    
	/**
	 * Global configuration of the Editor
	 * see https://www.drawio.com/doc/faq/configure-diagram-editor
	 * 
	 * For defaultVertexStyle, defaultEdgeStyle and defaultLibraries, this must be called before
	 * mxSettings.load via global config variable window.mxLoadSettings = false.
	 */
	Editor.configure = function(config)
	{
		if (config != null)
		{
			Editor.config = config;
			Editor.configVersion = config.version;

			// Enables debug output
			if (config.debug)
			{
				urlParams['test'] = '1'
			}

			// Inline embed mode options
			if (config.passiveScroll)
			{
				Editor.passiveScroll = true;
			}

			if (config.noResizers)
			{
				Editor.noResizers = true;
			}

			if (config.preserveViewState)
			{
				Editor.preserveViewState = true;
			}

			if (config.useInternalClipboard)
			{
				EditorUi.prototype.useInternalClipboard = config.useInternalClipboard;
			}

			if (config.customCss != null)
			{
				var style = document.createElement('style');
				style.type = 'text/css';
				style.appendChild(document.createTextNode(config.customCss));
				document.head.appendChild(style);
			}
			
			if (config.enableLocalFonts != null)
			{
				Editor.enableLocalFonts = config.enableLocalFonts;
			}

			if (config.defaultFonts != null)
			{
				Menus.prototype.defaultFonts = config.defaultFonts
			}

			if (config.presetColors != null)
			{
				ColorDialog.prototype.presetColors = config.presetColors
			}

			if (config.defaultColors != null)
			{
				ColorDialog.prototype.defaultColors = config.defaultColors
			}

			if (config.colorNames != null)
			{
				ColorDialog.prototype.colorNames = config.colorNames
			}

			if (config.defaultColorSchemes != null)
			{
				StyleFormatPanel.prototype.defaultColorSchemes = config.defaultColorSchemes
			}

			if (config.defaultEdgeLength != null)
			{
				Graph.prototype.defaultEdgeLength = config.defaultEdgeLength
			}

			if (config.selectParentLayer != null)
			{
				Graph.selectParentLayer = config.selectParentLayer
			}

			if (config.intersectionSelect != null)
			{
				Graph.intersectionSelect = config.intersectionSelect
			}

			if (config.autosaveDelay != null)
			{
				DrawioFile.prototype.autosaveDelay = config.autosaveDelay
			}
			
			if (config.templateFile != null)
			{
				EditorUi.templateFile = config.templateFile;
			}
			
			if (config.styles != null)
			{
				if (Array.isArray(config.styles))
				{
					Editor.styles = config.styles;
				}
				else
				{
					EditorUi.debug('Configuration Error: Array expected for styles');
				}
			}
			
			if (config.globalVars != null)
			{
				Editor.globalVars = config.globalVars;
			}

			if (config.defaultFileType != null)
			{
				Editor.defaultFileType = config.defaultFileType;
			}

			if (config.compressXml != null)
			{
				Editor.defaultCompressed = config.compressXml;
				Editor.compressXml = config.compressXml;
			}
			
			if (config.includeDiagram != null)
			{
				Editor.defaultIncludeDiagram = config.includeDiagram;
			}
			
			if (config.simpleLabels != null)
			{
				Editor.simpleLabels = config.simpleLabels;
			}

			if (config.optimizeHtmlLabels != null)
			{
				Editor.optimizeHtmlLabels = config.optimizeHtmlLabels;
			}

			if (config.pasteAtMousePointer != null)
			{
				Editor.pasteAtMousePointer = config.pasteAtMousePointer;
			}
			
			if (config.fitDiagramOnLoad != null)
			{
				Editor.fitDiagramOnLoad = config.fitDiagramOnLoad;
			}

			if (config.fitDiagramOnPage != null)
			{
				Editor.fitDiagramOnPage = config.fitDiagramOnPage;
			}

			if (config.showLinkIcons != null)
			{
				Editor.showLinkIcons = config.showLinkIcons;
			}

			if (config.showTooltipIcons != null)
			{
				Editor.showTooltipIcons = config.showTooltipIcons;
			}

			if (config.expandPatternsForPrint != null)
			{
				Editor.expandPatternsForPrint = config.expandPatternsForPrint;
			}

			if (config.showConnectHandle != null)
			{
				Editor.showConnectHandle = config.showConnectHandle;
			}

			if (config.enableInlineToolbar != null)
			{
				Editor.enableInlineToolbar = config.enableInlineToolbar;
			}
			
			if (config.oneDriveInlinePicker != null)
			{
				Editor.oneDriveInlinePicker = config.oneDriveInlinePicker;
			}
			
			if (config.enableNativeClipboard != null)
			{
				Editor.enableNativeClipboard = config.enableNativeClipboard;
			}
			
			if (config.enableExportUrl != null)
			{
				Editor.enableExportUrl = config.enableExportUrl;
			}

			if (config.defaultAdaptiveColors != null)
			{
				Graph.defaultAdaptiveColors = config.defaultAdaptiveColors;
			}
			else if (config.enableCssDarkMode == false)
			{
				// Backwards compatibility
				Graph.defaultAdaptiveColors = 'simple';
			}

			if (config.enableLightDarkColors != null)
			{
				mxUtils.lightDarkColorSupported = config.enableLightDarkColors;
			}
			
			if (config.darkColor != null)
			{
				Editor.darkColor = config.darkColor;
			}

			if (config.darkColorVar != null)
			{
				Editor.darkColorVar = config.darkColorVar;
			}

			// Updates colors that depend on Editor.darkColor
			// LATER: Add event to update darkColor dependencies
			Graph.prototype.defaultPageBackgroundColor = 'light-dark(#ffffff, ' + Editor.darkColor + ')';
			Graph.prototype.shapeBackgroundColor = 'light-dark(#ffffff, var(' +
				Editor.darkColorVar + ', ' + Editor.darkColor + '))';
			
			if (config.settingsName != null)
			{
				Editor.configurationKey = '.' + config.settingsName + '-configuration';
				Editor.settingsKey = '.' + config.settingsName + '-config';
				mxSettings.key = Editor.settingsKey;
			}
			
			if (config.customFonts != null)
			{
				Menus.prototype.defaultFonts = config.customFonts.
					concat(Menus.prototype.defaultFonts);
			}
			
			if (config.customPresetColors != null)
			{
				ColorDialog.prototype.presetColors = config.customPresetColors.
					concat(ColorDialog.prototype.presetColors);
			}
			
			if (config.customColorSchemes != null)
			{
				StyleFormatPanel.prototype.defaultColorSchemes = config.customColorSchemes.
					concat(StyleFormatPanel.prototype.defaultColorSchemes);
			}
			
			// Custom CSS injected directly into the page
			if (config.css != null)
			{
				var s = document.createElement('style');
				s.setAttribute('type', 'text/css');
				s.appendChild(document.createTextNode(config.css));
				
				var t = document.getElementsByTagName('script')[0];
			  	t.parentNode.insertBefore(s, t);
			}

			if (config.expandLibraries != null)
			{
				Sidebar.prototype.expandLibraries = config.expandLibraries;
			}
			
			if (config.appendCustomLibraries != null)
			{
				Sidebar.prototype.appendCustomLibraries = config.appendCustomLibraries;
			}
			
			// Configures the custom libraries
			if (config.libraries != null)
			{
				Sidebar.prototype.customEntries = config.libraries;
			}
			
			// Defines the enabled built-in libraries.
			if (config.enabledLibraries != null)
			{
				if (Array.isArray(config.enabledLibraries))
				{
					Sidebar.prototype.enabledLibraries = config.enabledLibraries;
				}
				else
				{
					EditorUi.debug('Configuration Error: Array expected for enabledLibraries');
				}
			}
			
			// Overrides default libraries
			if (config.defaultLibraries != null)
			{
				Sidebar.prototype.defaultEntries = config.defaultLibraries;
			}
			
			// Overrides default custom libraries
			if (config.defaultCustomLibraries != null)
			{
				Editor.defaultCustomLibraries = config.defaultCustomLibraries;
			}
			
			// Disables custom libraries
			if (config.enableCustomLibraries != null)
			{
				Editor.enableCustomLibraries = config.enableCustomLibraries;
			}
			
			// Overrides default vertex style
			if (config.defaultVertexStyle != null)
			{
				Graph.prototype.defaultVertexStyle = config.defaultVertexStyle;
			}

			// Overrides default edge style
			if (config.defaultEdgeStyle != null)
			{
				Graph.prototype.defaultEdgeStyle = config.defaultEdgeStyle;
			}

			// Overrides default shape picker entries
			if (config.shapePicker != null)
			{
				EditorUi.prototype.defaultShapePickerEntries = config.shapePicker.shapes;
			}

			// Overrides default page visible
			if (config.defaultPageVisible != null)
			{
				Graph.prototype.defaultPageVisible = config.defaultPageVisible;
			}

			// Overrides default grid enabled
			if (config.defaultGridEnabled != null)
			{
				Graph.prototype.defaultGridEnabled = config.defaultGridEnabled;
			}

			// Overrides default connection arrows on hover
			if (config.defaultConnectionArrowsEnabled != null)
			{
				Graph.prototype.defaultConnectionArrowsEnabled = config.defaultConnectionArrowsEnabled;
				Graph.prototype.connectionArrowsEnabled = config.defaultConnectionArrowsEnabled;
			}

			// Overrides default connectable (connection handler)
			if (config.defaultConnectable != null)
			{
				Graph.prototype.defaultConnectable = config.defaultConnectable;
			}

			// Overrides default folding enabled
			if (config.defaultFoldingEnabled != null)
			{
				Graph.prototype.defaultFoldingEnabled = config.defaultFoldingEnabled;
			}

			// Overrides mouse wheel function
			if (config.zoomWheel != null)
			{
				Graph.zoomWheel = config.zoomWheel;
			}

			// Overrides zoom factor
			if (config.zoomFactor != null)
			{
				var val = parseFloat(config.zoomFactor);
				
				if (!isNaN(val) && val > 1)
				{
					Graph.prototype.zoomFactor = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Float > 1 expected for zoomFactor');
				}
			}

			// Overrides default grid size
			if (config.defaultGridSize != null)
			{
				var val = parseInt(config.defaultGridSize);
				
				if (!isNaN(val) && val > 0)
				{
					mxGraph.prototype.gridSize = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for defaultGridSize');
				}
			}

			// Overrides grid steps
			if (config.gridSteps != null)
			{
				var val = parseInt(config.gridSteps);
				
				if (!isNaN(val) && val > 0)
				{
					mxGraphView.prototype.gridSteps = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for gridSteps');
				}
			}

			if (config.pageFormat != null)
			{
				var w = parseInt(config.pageFormat.width);
				var h = parseInt(config.pageFormat.height);

				if (!isNaN(w) && w > 0 && !isNaN(h) && h > 0)
				{
					mxGraph.prototype.defaultPageFormat = new mxRectangle(0, 0, w, h);
					mxGraph.prototype.pageFormat = mxGraph.prototype.defaultPageFormat;
				}
				else
				{
					EditorUi.debug('Configuration Error: {width: int, height: int} expected for pageFormat');
				}
			}
			
			if (config.thumbWidth != null)
			{
				Sidebar.prototype.thumbWidth = config.thumbWidth;
			}
			
			if (config.thumbHeight != null)
			{
				Sidebar.prototype.thumbHeight = config.thumbHeight;
			}
			
			if (config.emptyLibraryXml != null)
			{
				EditorUi.prototype.emptyLibraryXml = config.emptyLibraryXml;
			}

			if (config.emptyDiagramXml != null)
			{
				EditorUi.prototype.emptyDiagramXml = config.emptyDiagramXml;
			}
			
			if (config.sidebarWidth != null)
			{
				EditorUi.prototype.hsplitPosition = config.sidebarWidth;
			}

			if (config.updateDefaultStyle != null)
			{
				EditorUi.prototype.updateDefaultStyle = config.updateDefaultStyle;
			}
			
			if (config.sidebarTitles != null)
			{
				Sidebar.prototype.sidebarTitles = config.sidebarTitles;
			}
			
			if (config.sidebarTitleSize != null)
			{
				var val = parseInt(config.sidebarTitleSize);
				
				if (!isNaN(val) && val > 0)
				{
					Sidebar.prototype.sidebarTitleSize = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for sidebarTitleSize');
				}
			}
			
			if (config.fontCss != null)
			{
				if (typeof config.fontCss === 'string')
				{
					Editor.configureFontCss(config.fontCss);
				}
				else
				{
					EditorUi.debug('Configuration Error: String expected for fontCss');
				}
			}
			
			if (config.autosaveDelay != null)
			{
				var val = parseInt(config.autosaveDelay);
				
				if (!isNaN(val) && val > 0)
				{
					DrawioFile.prototype.autosaveDelay = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for autosaveDelay');
				}
			}

			if(config.maxImageBytes != null) 
			{
				EditorUi.prototype.maxImageBytes = config.maxImageBytes;
			}
			
			if(config.maxImageSize != null) 
			{
				EditorUi.prototype.maxImageSize = config.maxImageSize;
			}
			
			if (config.shareCursorPosition != null)
			{
				EditorUi.prototype.shareCursorPosition = config.shareCursorPosition;
			}

			if (config.showRemoteCursors != null)
			{
				EditorUi.prototype.showRemoteCursors = config.showRemoteCursors;
			}

			if (config.restrictExport != null)
			{
				DrawioFile.RESTRICT_EXPORT = config.restrictExport;
			}

			if (config.removeImageMetadata != null)
			{
				Editor.removeImageMetadata = config.removeImageMetadata;
			}

			if (config.defaultTextStyle != null)
			{
				Editor.defaultTextStyle = config.defaultTextStyle;
			}
			
			if (config.replaceSvgDataUris != null)
			{
				Editor.replaceSvgDataUris = config.replaceSvgDataUris;
			}

			if (config.foreignObjectImages != null)
			{
				Editor.foreignObjectImages = config.foreignObjectImages;
			}

			if (config.shadowColor != null)
			{
				mxConstants.SHADOW_COLOR = config.shadowColor;
			}

			if (config.shadowOpacity != null)
			{
				mxConstants.SHADOW_OPACITY = config.shadowOpacity;
			}

			if (config.shadowOffsetX != null)
			{
				mxConstants.SHADOW_OFFSET_X = config.shadowOffsetX;
			}

			if (config.shadowOffsetY != null)
			{
				mxConstants.SHADOW_OFFSET_Y = config.shadowOffsetY;
			}

			if (config.shadowBlur != null)
			{
				mxConstants.SHADOW_BLUR = config.shadowBlur;
			}

			if (config.enableAnimations != null)
			{
				Editor.enableAnimations = config.enableAnimations;
			}

			if (config.enableWindowDocking != null)
			{
				Editor.enableWindowDocking = config.enableWindowDocking;
			}

			if (config.compact != null)
			{
				EditorUi.prototype.defaultCompact = config.compact;
			}

			if (config.noAutoFocus != null)
			{
				EditorUi.prototype.noAutoFocus = config.noAutoFocus;
			}

			if (config.hideMenuItems != null)
			{
				if (Array.isArray(config.hideMenuItems))
				{
					Menus.prototype.hiddenMenuItems = {};

					for (var i = 0; i < config.hideMenuItems.length; i++)
					{
						Menus.prototype.hiddenMenuItems[config.hideMenuItems[i]] = true;
					}
				}
				else
				{
					EditorUi.debug('Configuration Error: Array expected for hideMenuItems');
				}
			}

			if (config.hideMenus != null)
			{
				if (Array.isArray(config.hideMenus))
				{
					var hiddenLookup = {};

					for (var i = 0; i < config.hideMenus.length; i++)
					{
						hiddenLookup[config.hideMenus[i]] = true;
					}

					Menus.prototype.defaultMenuItems = Menus.prototype.defaultMenuItems.filter(function(m)
					{
						return !hiddenLookup[m];
					});
				}
				else
				{
					EditorUi.debug('Configuration Error: Array expected for hideMenus');
				}
			}

			if (config.enableAi != null)
			{
				Editor.enableAi = config.enableAi;
			}

			if (config.enableChatGpt != null)
			{
				Editor.enableAi = config.enableChatGpt;
			}

			if (config.gptApiKey != null)
			{
				Editor.aiGlobals['gptApiKey'] = config.gptApiKey;
			}

			if (config.geminiApiKey != null)
			{
				Editor.aiGlobals['geminiApiKey'] = config.geminiApiKey;
			}

			if (config.claudeApiKey != null)
			{
				Editor.aiGlobals['claudeApiKey'] = config.claudeApiKey;
			}

			if (config.gptUrl != null && Editor.aiConfigs['gpt'] != null)
			{
				Editor.aiConfigs['gpt'].endpoint = config.gptUrl;
			}

			if (config.aiActions != null)
			{
				Editor.aiActions = config.aiActions;
			}

			if (config.aiGlobals != null)
			{
				Editor.aiGlobals = config.aiGlobals;
			}

			if (config.aiConfigs != null)
			{
				Editor.aiConfigs = config.aiConfigs;
			}

			if (config.aiModels != null)
			{
				Editor.aiModels = config.aiModels;
			}
		}
	};
	
	/**
	 * 
	 */
	Editor.isSettingsEnabled = function()
	{
		return typeof window.mxSettings !== 'undefined' && (isLocalStorage || mxClient.IS_CHROMEAPP);
	};

	/**
	 * Expands SVG fill patterns into inline vector geometry clipped to each
	 * shape so that patterns are not rasterized by Chrome's PDF backend.
	 */
	Editor.expandSvgPatterns = function(svg)
	{
		var svgNS = 'http://www.w3.org/2000/svg';
		var clipCounter = 0;

		/**
		 * Parses a patternTransform attribute string into its components.
		 * Expected form: "translate(tx,ty) rotate(angle) scale(s)"
		 */
		function parsePatternTransform(str)
		{
			var tx = 0, ty = 0, angle = 0, sx = 1, sy = 1;

			if (str != null)
			{
				var translateMatch = str.match(/translate\(\s*([^,\s]+)[,\s]+([^)]+)\)/);

				if (translateMatch != null)
				{
					tx = parseFloat(translateMatch[1]);
					ty = parseFloat(translateMatch[2]);
				}

				var rotateMatch = str.match(/rotate\(\s*([^)]+)\)/);

				if (rotateMatch != null)
				{
					angle = parseFloat(rotateMatch[1]) * Math.PI / 180;
				}

				var scaleMatch = str.match(/scale\(\s*([^,\s)]+)(?:[,\s]+([^)]+))?\)/);

				if (scaleMatch != null)
				{
					sx = parseFloat(scaleMatch[1]);
					sy = (scaleMatch[2] != null) ? parseFloat(scaleMatch[2]) : sx;
				}
			}

			return {tx: tx, ty: ty, angle: angle, sx: sx, sy: sy};
		};

		/**
		 * Transforms a point from pattern space to user space.
		 */
		function transformPoint(x, y, t)
		{
			// Apply scale
			var sx = x * t.sx;
			var sy = y * t.sy;

			// Apply rotation
			var cos = Math.cos(t.angle);
			var sin = Math.sin(t.angle);
			var rx = sx * cos - sy * sin;
			var ry = sx * sin + sy * cos;

			// Apply translation
			return {x: rx + t.tx, y: ry + t.ty};
		};

		/**
		 * Transforms a point from user space to pattern space (inverse).
		 */
		function inverseTransformPoint(x, y, t)
		{
			// Remove translation
			var dx = x - t.tx;
			var dy = y - t.ty;

			// Inverse rotation
			var cos = Math.cos(-t.angle);
			var sin = Math.sin(-t.angle);
			var rx = dx * cos - dy * sin;
			var ry = dx * sin + dy * cos;

			// Inverse scale
			return {x: rx / t.sx, y: ry / t.sy};
		};

		/**
		 * Returns the accumulated transform from an element up to the SVG root
		 * as a transform attribute string, or null if no transforms exist.
		 */
		function getAccumulatedTransform(el, svgRoot)
		{
			var transforms = [];
			var current = el;

			while (current != null && current !== svgRoot && current.nodeType === 1)
			{
				var t = current.getAttribute('transform');

				if (t != null && t.length > 0)
				{
					transforms.unshift(t);
				}

				current = current.parentNode;
			}

			return (transforms.length > 0) ? transforms.join(' ') : null;
		};

		/**
		 * Resolves a fill URL reference to a pattern element.
		 */
		function resolvePattern(el, svgRoot)
		{
			var fill = el.getAttribute('fill');

			if (fill == null || fill.indexOf('url(') !== 0)
			{
				return null;
			}

			var match = fill.match(/url\([^#]*#([^)"]+)\)?/);

			if (match == null)
			{
				return null;
			}

			var patternId = match[1];

			// Uses querySelector on SVG root instead of getElementById
			// because the print preview document may not have indexed IDs
			try
			{
				var pattern = svgRoot.querySelector('#' + CSS.escape(patternId));

				return (pattern != null && pattern.nodeName === 'pattern') ? pattern : null;
			}
			catch (e)
			{
				return null;
			}
		};

		/**
		 * Expands a single pattern-filled element.
		 */
		function expandElement(el, pattern, svgRoot)
		{
			var bbox;

			try
			{
				bbox = el.getBBox();
			}
			catch (e)
			{
				return;
			}

			if (bbox.width === 0 || bbox.height === 0)
			{
				return;
			}

			var pw = parseFloat(pattern.getAttribute('width'));
			var ph = parseFloat(pattern.getAttribute('height'));

			if (isNaN(pw) || isNaN(ph) || pw <= 0 || ph <= 0)
			{
				return;
			}

			var t = parsePatternTransform(pattern.getAttribute('patternTransform'));

			// Get the element's accumulated transform for the clip path
			var elemTransform = getAccumulatedTransform(el, svgRoot);

			// Compute bbox corners in user space (relative to element's local coords)
			var corners = [
				{x: bbox.x, y: bbox.y},
				{x: bbox.x + bbox.width, y: bbox.y},
				{x: bbox.x + bbox.width, y: bbox.y + bbox.height},
				{x: bbox.x, y: bbox.y + bbox.height}
			];

			// If the element has a transform, apply it to get coordinates in
			// the same space as the pattern (user/diagram space)
			if (elemTransform != null)
			{
				try
				{
					var ctm = el.getCTM();
					var svgCtm = svgRoot.getCTM() || svgRoot.createSVGMatrix();
					// Transform from element space to SVG space
					var m = svgCtm.inverse().multiply(ctm);

					for (var i = 0; i < corners.length; i++)
					{
						var pt = svgRoot.createSVGPoint();
						pt.x = corners[i].x;
						pt.y = corners[i].y;
						pt = pt.matrixTransform(m);
						corners[i] = {x: pt.x, y: pt.y};
					}
				}
				catch (e)
				{
					// Fall back to untransformed corners
				}
			}

			// Transform corners to pattern space (inverse of patternTransform)
			var minX = Infinity, minY = Infinity;
			var maxX = -Infinity, maxY = -Infinity;

			for (var i = 0; i < corners.length; i++)
			{
				var p = inverseTransformPoint(corners[i].x, corners[i].y, t);
				minX = Math.min(minX, p.x);
				minY = Math.min(minY, p.y);
				maxX = Math.max(maxX, p.x);
				maxY = Math.max(maxY, p.y);
			}

			// Compute tile index range with margin
			var tileMinX = Math.floor(minX / pw) - 1;
			var tileMinY = Math.floor(minY / ph) - 1;
			var tileMaxX = Math.ceil(maxX / pw) + 1;
			var tileMaxY = Math.ceil(maxY / ph) + 1;

			// Safety limit to prevent excessive tiles
			var maxTiles = 10000;
			var tileCount = (tileMaxX - tileMinX) * (tileMaxY - tileMinY);

			if (tileCount > maxTiles)
			{
				return;
			}

			// Create the clip path from the element
			var clipId = 'expand-pattern-clip-' + (++clipCounter);
			var clipPath = svgRoot.ownerDocument.createElementNS(svgNS, 'clipPath');
			clipPath.setAttribute('id', clipId);

			var clipShape = el.cloneNode(true);
			clipShape.removeAttribute('fill');
			clipShape.removeAttribute('fill-opacity');
			clipShape.removeAttribute('stroke');
			clipShape.removeAttribute('stroke-width');
			clipShape.removeAttribute('stroke-opacity');
			clipShape.removeAttribute('style');
			clipShape.removeAttribute('pointer-events');
			clipShape.setAttribute('fill', 'black');
			clipPath.appendChild(clipShape);

			// Create outer group with clip path
			var outerGroup = svgRoot.ownerDocument.createElementNS(svgNS, 'g');
			outerGroup.setAttribute('clip-path', 'url(#' + clipId + ')');

			// Preserve fill-opacity from the original element
			var fillOpacity = el.getAttribute('fill-opacity');

			if (fillOpacity != null)
			{
				outerGroup.setAttribute('opacity', fillOpacity);
			}

			// The clip path is in the element's parent coordinate space,
			// so the outer group should be at the same level
			// Create inner group with patternTransform
			var innerGroup = svgRoot.ownerDocument.createElementNS(svgNS, 'g');
			innerGroup.setAttribute('transform',
				'translate(' + t.tx + ',' + t.ty + ')' +
				((t.angle !== 0) ? ' rotate(' + (t.angle * 180 / Math.PI) + ')' : '') +
				' scale(' + t.sx + ((t.sy !== t.sx) ? ',' + t.sy : '') + ')');

			// Generate tiles by cloning pattern children
			var patternChildren = pattern.childNodes;

			for (var tx = tileMinX; tx < tileMaxX; tx++)
			{
				for (var ty = tileMinY; ty < tileMaxY; ty++)
				{
					var tileGroup = svgRoot.ownerDocument.createElementNS(svgNS, 'g');
					tileGroup.setAttribute('transform',
						'translate(' + (tx * pw) + ',' + (ty * ph) + ')');

					for (var c = 0; c < patternChildren.length; c++)
					{
						if (patternChildren[c].nodeType === 1)
						{
							tileGroup.appendChild(patternChildren[c].cloneNode(true));
						}
					}

					innerGroup.appendChild(tileGroup);
				}
			}

			outerGroup.appendChild(innerGroup);

			// Insert defs and group into SVG
			var defs = svgRoot.querySelector('defs');

			if (defs == null)
			{
				defs = svgRoot.ownerDocument.createElementNS(svgNS, 'defs');
				svgRoot.insertBefore(defs, svgRoot.firstChild);
			}

			defs.appendChild(clipPath);

			// Insert the pattern group after the element's parent group
			// to maintain correct stacking order
			el.parentNode.insertBefore(outerGroup, el.nextSibling);

			// Remove the pattern fill from the element. The pattern's
			// transparent regions are truly transparent (the fill color is
			// used as the pattern line color, not as a background).
			el.setAttribute('fill', 'none');
			el.style.fill = '';
		};

		// Main: find all pattern-filled elements and expand them
		var elements = svg.querySelectorAll('[fill^="url("]');

		for (var i = 0; i < elements.length; i++)
		{
			var pattern = resolvePattern(elements[i], svg);

			if (pattern != null)
			{
				expandElement(elements[i], pattern, svg);
			}
		}

		// Also check elements with fill set via style attribute
		var styledElements = svg.querySelectorAll('[style]');

		for (var i = 0; i < styledElements.length; i++)
		{
			var style = styledElements[i].getAttribute('style');

			if (style != null && style.indexOf('url(') !== -1)
			{
				var pattern = resolvePattern(styledElements[i], svg);

				if (pattern != null)
				{
					expandElement(styledElements[i], pattern, svg);
				}
			}
		}
	};

	/**
	 * Adds the global fontCss configuration.
	 */
	Editor.configureFontCss = function(fontCss)
	{
		if (fontCss != null)
		{
			Editor.prototype.fontCss = fontCss;
			var t = document.getElementsByTagName('script')[0];
			
			if (t != null && t.parentNode != null)
			{
				var s = document.createElement('style');
				s.setAttribute('type', 'text/css');
				s.appendChild(document.createTextNode(fontCss));
				t.parentNode.insertBefore(s, t);
				
				// Preloads fonts where supported
				var parts = fontCss.split('url(');
				
				for (var i = 1; i < parts.length; i++)
				{
				    var idx = parts[i].indexOf(')');
				    var url = Editor.trimCssUrl(parts[i].substring(0, idx));
				    
				    var l = document.createElement('link');
					l.setAttribute('rel', 'preload');
					l.setAttribute('href', url);
					l.setAttribute('as', 'font');
					l.setAttribute('crossorigin', '');
					
				  	t.parentNode.insertBefore(l, t);
				}
			}
		}
	};
			
	/**
	 * Strips leading and trailing quotes and spaces
	 */
    Editor.trimCssUrl = function(str)
    {
    	return str.replace(new RegExp("^[\\s\"']+", "g"), "").replace(new RegExp("[\\s\"']+$", "g"), "");
    }
    
    /**
     * Prefix for URLs that reference Google fonts.
     */
	Editor.GOOGLE_FONTS = 'https://fonts.googleapis.com/css?family=';
     
    /**
     * Prefix for URLs that reference Google fonts with CSS2.
     */
	Editor.GOOGLE_FONTS_CSS2 = 'https://fonts.googleapis.com/css2?family=';
    
	/**
	 * Alphabet for global unique IDs.
	 */
	Editor.GUID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_';

	/**
	 * Default length for global unique IDs.
	 */
	Editor.GUID_LENGTH = 20;
	
	/**
	 * Default length for global unique IDs.
	 */
	Editor.guid = function(length)
	{
		var len = (length != null) ? length : Editor.GUID_LENGTH;
		var rtn = [];
	  
		for (var i = 0; i < len; i++)
		{
			rtn.push(Editor.GUID_ALPHABET.charAt(Math.floor(Math.random() * Editor.GUID_ALPHABET.length)));
		}

		return rtn.join('');
	};

	/**
	 * Interval for updating the file status.
	 */
	Editor.updateStatusInterval = 10000;

	/**
	 * Specifies the app name. Default is document.title.
	 */
	Editor.prototype.appName = 'draw.io';
		
	/**
	 * Known file types.
	 */
	Editor.prototype.diagramFileTypes = [
		{description: 'diagramXmlDesc', extension: 'drawio', mimeType: 'text/xml'},
		{description: 'diagramPngDesc', extension: 'png', mimeType: 'image/png'},
		{description: 'diagramSvgDesc', extension: 'svg', mimeType: 'image/svg'},
		{description: 'diagramHtmlDesc', extension: 'html', mimeType: 'text/html'}];
	
	/**
	 * Known file types.
	 */
	Editor.prototype.libraryFileTypes = [{description: 'Library (.drawiolib, .xml)', extensions: ['drawiolib', 'xml']}];
 
	/**
	 * Additional help text for special file extensions.
	 */
	Editor.prototype.fileExtensions = [
		{ext: 'html', title: 'filetypeHtml'},
		{ext: 'png', title: 'filetypePng'},
		{ext: 'svg', title: 'filetypeSvg'}];
	
	/**
	 * General timeout is 25 seconds.
	 */
	Editor.prototype.timeout = 25000;
	
	/**
	 * AI backend timeout in seconds.
	 */
	Editor.prototype.generateTimeout = 60000;
	
	/**
	 * Executes the first step for connecting to Google Drive.
	 */
	Editor.prototype.editButtonLink = (urlParams['edit'] != null) ? decodeURIComponent(urlParams['edit']) : null;

	/**
	 * Specifies if img.crossOrigin is supported.
	 */
	Editor.prototype.crossOriginImages = true;
	
	/**
	 * Adds support for old stylesheets and compressed files
	 */
	var editorSetGraphXml = Editor.prototype.setGraphXml;
	Editor.prototype.setGraphXml = function(node)
	{
		node = (node != null && node.nodeName != 'mxlibrary') ? this.extractGraphModel(node) : null;

		if (node != null)
		{
			// Checks for parser errors
			var cause = Editor.extractParserError(node, mxResources.get('invalidOrMissingFile'));

			if (cause)
			{
				EditorUi.debug('Editor.setGraphXml ParserError', [this],
					'node', [node], 'cause', [cause]);

				throw new Error(mxResources.get('notADiagramFile') + ' (' + cause + ')');
			}
			else if (node.nodeName == 'mxGraphModel')
			{
				var style = node.getAttribute('style') || 'default-style2';
				
				// Decodes the style if required
				if (urlParams['embed'] != '1' && (style == null || style == ''))
				{
					var node2 = (this.graph.themes != null) ?
						this.graph.themes['default-old'] :
						mxUtils.load(STYLE_PATH + '/default-old.xml').getDocumentElement();
				    
				    if (node2 != null)
				    {
				    	var dec2 = new mxCodec(node2.ownerDocument);
				    	dec2.decode(node2, this.graph.getStylesheet());
				    }
				}
				else if (style != this.graph.currentStyle)
				{
				    var node2 = (this.graph.themes != null) ?
						this.graph.themes[style] :
						mxUtils.load(STYLE_PATH + '/' + style + '.xml').getDocumentElement()
				    
				    if (node2 != null)
				    {
				    	var dec2 = new mxCodec(node2.ownerDocument);
				    	dec2.decode(node2, this.graph.getStylesheet());
				    }
				}
	
				this.graph.currentStyle = style;
				this.graph.mathEnabled = (urlParams['math'] == '1' || node.getAttribute('math') == '1');
				this.graph.setAdaptiveColors(node.getAttribute('adaptiveColors'));
				
				var bgImg = node.getAttribute('backgroundImage');
				
				if (bgImg != null)
				{
					this.graph.setBackgroundImage(this.graph.parseBackgroundImage(bgImg));
				}
				else
				{
					this.graph.setBackgroundImage(null);
				}
				
				this.graph.useCssTransforms = !mxClient.NO_FO &&
					this.isChromelessView() &&
					this.graph.isCssTransformsSupported();
				this.graph.updateCssTransform();

				this.graph.setShadowVisible(node.getAttribute('shadow') == '1', false);
				
				var extFonts = node.getAttribute('extFonts');
				
				if (extFonts)
				{
					try
					{
						extFonts = extFonts.split('|').map(function(ef)
						{
							var parts = ef.split('^');
							return {name: parts[0], url: parts[1]};
						});
						
						for (var i = 0; i < extFonts.length; i++)
						{
							this.graph.addExtFont(extFonts[i].name, extFonts[i].url);
						}
					}
					catch(e)
					{
						console.log('ExtFonts format error: ' + e.message);
					}
				}
				else if (this.graph.extFonts != null && this.graph.extFonts.length > 0)
				{
					this.graph.extFonts = [];
				}
			}
	
			// Calls updateGraphComponents
			editorSetGraphXml.apply(this, arguments);
		}
		else
		{
			throw { 
			    message: mxResources.get('notADiagramFile') || 'Invalid data',
			    toString: function() { return this.message; }
			};
		}
	};

	/**
	 * Adds persistent style to file
	 */
	var editorGetGraphXml = Editor.prototype.getGraphXml;	
	Editor.prototype.getGraphXml = function(ignoreSelection, resolveReferences)
	{
		ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
		var node = editorGetGraphXml.apply(this, arguments);
		
		// Adds the current style
		if (this.graph.currentStyle != null && this.graph.currentStyle != 'default-style2')
		{
			node.setAttribute('style', this.graph.currentStyle);
		}

		var bgImg = this.graph.getBackgroundImageObject(
			this.graph.backgroundImage,
			resolveReferences);
		
		// Adds the background image
		if (bgImg != null)
		{
			node.setAttribute('backgroundImage', JSON.stringify(bgImg));
		}
		
		node.setAttribute('math', (this.graph.mathEnabled) ? '1' : '0');
		node.setAttribute('shadow', (this.graph.shadowVisible) ? '1' : '0');

		if (this.graph.adaptiveColors != null)
		{
			node.setAttribute('adaptiveColors', this.graph.adaptiveColors);
		}
		else
		{
			node.removeAttribute('adaptiveColors');
		}
		
		if (this.graph.extFonts != null && this.graph.extFonts.length > 0)
		{
			var strExtFonts = this.graph.extFonts.map(function(ef)
			{
				return ef.name + '^' + ef.url;
			});
			
			node.setAttribute('extFonts', strExtFonts.join('|'));
		}
		
		return node;
	};
	
	/**
	 * Helper function to extract the graph model XML node.
	 */
	Editor.prototype.isDataSvg = function(svg)
	{
		try
		{
			var svgRoot = mxUtils.parseXml(svg).documentElement;
			var tmp = svgRoot.getAttribute('content');
			
			if (tmp != null)
			{
				if (tmp != null && tmp.charAt(0) != '<' && tmp.charAt(0) != '%')
				{
					tmp = unescape((window.atob) ? atob(tmp) : Base64.decode(cont, tmp));
				}
				
				if (tmp != null && tmp.charAt(0) == '%')
				{
					tmp = decodeURIComponent(tmp);
				}
				
				if (tmp != null && tmp.length > 0)
				{
					var node = mxUtils.parseXml(tmp).documentElement;
					
					return node.nodeName == 'mxfile' || node.nodeName == 'mxGraphModel';
				}
			}
		}
		catch (e)
		{
			// ignore
		}
		
		return false;
	};
	
	/**
	 * Helper function to extract the graph model XML node.
	 */
	Editor.prototype.extractGraphModel = function(node, allowMxFile, checked)
	{
		return Editor.extractGraphModel.apply(this, arguments);
	};
	
	/**
	 * Overrides reset graph.
	 */
	var editorResetGraph = Editor.prototype.resetGraph;	
	Editor.prototype.resetGraph = function()
	{
		this.graph.mathEnabled = (urlParams['math'] == '1');
		this.graph.setAdaptiveColors(null);
		this.graph.view.x0 = null;
		this.graph.view.y0 = null;
		
		this.graph.useCssTransforms = !mxClient.NO_FO &&
			this.isChromelessView() &&
			this.graph.isCssTransformsSupported();
		this.graph.updateCssTransform();
		
		editorResetGraph.apply(this, arguments);
	};

	/**
	 * Math support.
	 */
	var editorUpdateGraphComponents = Editor.prototype.updateGraphComponents;
	Editor.prototype.updateGraphComponents = function()
	{
		editorUpdateGraphComponents.apply(this, arguments);
		
		this.graph.useCssTransforms = !mxClient.NO_FO &&
			this.isChromelessView() &&
			this.graph.isCssTransformsSupported();
		this.graph.updateCssTransform();
	};
	
	/**
	 * Initializes math typesetting and loads respective code.
	 */
	Editor.initMath = function(src, config)
	{
		if (typeof window.MathJax === 'undefined')
		{
			src = (src != null) ? src : DRAW_MATH_URL + '/startup.js';
			Editor.mathJaxQueue = [];

			// Blocks concurrent rendering while
			// async rendering is in progress
			var rendering = null;

			function mathJaxDone()
			{
				rendering = null;

				if (Editor.mathJaxQueue.length > 0)
				{
					Editor.doMathJaxRender(Editor.mathJaxQueue.shift());
				}
				else
				{
					Editor.onMathJaxDone();
				}
			};
			
			Editor.doMathJaxRender = function(container)
			{
				// Disables automatic line breaking for inline math to
				// avoid unwanted breaks in narrow label containers
				if (MathJax.startup != null && MathJax.startup.output != null &&
					MathJax.startup.output.options != null &&
					MathJax.startup.output.options.linebreaks != null)
				{
					MathJax.startup.output.options.linebreaks.inline = false;
				}

				try
				{
					if (rendering == null)
					{
						MathJax.typesetClear([container]);
						MathJax.typeset([container]);
						mathJaxDone();
					}
					else if (rendering != container)
					{
						Editor.mathJaxQueue.push(container);
					}
				}
				catch (e)
				{
					MathJax.typesetClear([container]);

					if (e.retry != null)
					{
						rendering = container;

						e.retry.then(function()
						{
							MathJax.typesetPromise([container]).then(mathJaxDone)['catch'](function(e)
							{
								console.log('Error in MathJax.typesetPromise: ' + e.toString());
								mathJaxDone();
							});
						})['catch'](function(e)
						{
							console.log('Error in MathJax.retry: ' + e.toString());
							mathJaxDone();
						});;
					}
					else if (window.console != null)
					{
						console.log('Error in MathJax.typeset: ' + e.toString());
					}
				}
			};
			
			window.MathJax = (config != null) ? config :
			{
				options:
				{
					skipHtmlTags: {'[+]': ['text']},
					ignoreHtmlClass: 'geDisableMathJax'
				},
				loader:
				{
					load: [(urlParams['math-output'] == 'html') ?
						'output/chtml' : 'output/svg', 'input/tex',
						'input/asciimath', 'ui/safe'],
					paths: {
						'fonts': DRAW_MATH_URL + '/fonts'
					}
				},
				output: {
					font: 'mathjax-tex'
				},
				startup:
				{
					pageReady: function()
					{
						for (var i = 0; i < Editor.mathJaxQueue.length; i++)
						{
							Editor.doMathJaxRender(Editor.mathJaxQueue[i]);
						}
					}
				}
			};

			// Adds global enqueue method for async rendering
			Editor.MathJaxRender = function(container)
			{
				if (typeof MathJax !== 'undefined' && typeof MathJax.typeset === 'function')
				{
					Editor.doMathJaxRender(container);
				}
				else
				{
					Editor.mathJaxQueue.push(container);
				}
			};
			
			// Adds global MathJax render callback
			Editor.onMathJaxDone = function()
			{
				// Hook for listeners
			};

			// Updates math typesetting after changes
			var editorInit = Editor.prototype.init;
			
			Editor.prototype.init = function()
			{
				editorInit.apply(this, arguments);

				var renderMath = mxUtils.bind(this, function(sender, evt)
				{
					if (this.graph.container != null &&
						this.graph.mathEnabled)
					{
						Editor.MathJaxRender(this.graph.container);
					}
				});
				
				this.graph.model.addListener(mxEvent.CHANGE, renderMath);
				this.graph.addListener(mxEvent.REFRESH, renderMath);
			};
			
			var tags = document.getElementsByTagName('script');
			
			if (tags != null && tags.length > 0)
			{
				var s = document.createElement('script');
				s.setAttribute('type', 'text/javascript');
				s.setAttribute('src', src);
				tags[0].parentNode.appendChild(s);
			}
		}
	};

	/**
	 * Parses line of CSV values according to RFC 4180.
	 */
	Editor.prototype.csvToArray = function(text)
	{
		if (text.length > 0)
		{
			var p = '', row = [''], i = 0, s = !0, l;

			for (l of text)
			{
				if ('"' === l)
				{
					if (s && l === p)
					{
						row[i] += l;
					}

					s = !s;
				}
				else if (',' === l && s)
				{
					l = row[++i] = '';
				}
				else
				{
					row[i] += l;
				}

				p = l;
			}
			
			return row;
		}
		else
		{
			return [];
		}
	};

	/**
	 * Returns an URL that is proxied if the given URL is blocked. Uses
	 * direct URL if no CSP is used as proxy blocks unknown text content.
	 */
	Editor.prototype.getProxiedUrl = function(url)
	{
		if ((/test\.draw\.io$/.test(window.location.hostname) ||
			/app\.diagrams\.net$/.test(window.location.hostname)) &&
			!this.isCorsEnabledForUrl(url))
		{
			var isVisioFilename = EditorUi.isVisioFilename(url);
			var binary = /\.png$/i.test(url) || /\.pdf$/i.test(url);
			var base64 = binary || isVisioFilename;
			var nocache = 't=' + new Date().getTime();
			url = PROXY_URL + '?url=' + encodeURIComponent(url) +
				'&' + nocache + ((base64) ? '&base64=1' : '');
		}

		return url;
	};

	/**
	 * Returns true if the given URL is known to have CORS headers and is
	 * allowed by CSP.
	 */
	Editor.prototype.isCorsEnabledForUrl = function(url)
	{
		// Disables proxy for desktop and chrome app as it is served locally
		if (mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
		{
			return true;
		}
		
		// Does not use proxy for same domain
		if (url.substring(0, window.location.origin.length) == window.location.origin)
		{
			return true;
		}

		// Blocked by CSP in production but allowed for hosted deployment
		if (urlParams['cors'] != null && this.corsRegExp == null)
		{
			this.corsRegExp = new RegExp(decodeURIComponent(urlParams['cors']));
		}
		
		// No access-control-allow-origin for some Iconfinder images, add this when fixed:
		// /^https?:\/\/[^\/]*\.iconfinder.com\//.test(url) ||
		return (this.corsRegExp != null && this.corsRegExp.test(url)) ||
			url.substring(0, 34) === 'https://raw.githubusercontent.com/' ||
			url.substring(0, 29) === 'https://fonts.googleapis.com/' ||
			url.substring(0, 26) === 'https://fonts.gstatic.com/';
	};

	/**
	 * Converts all images in the SVG output to data URIs for immediate rendering
	 */
	Editor.prototype.createImageUrlConverter = function()
	{
		var converter = new mxUrlConverter();
		converter.updateBaseUrl();

		// Extends convert to avoid CORS using an image proxy server where needed
		var convert = converter.convert;
		var self = this;
		
		converter.convert = function(src)
		{
			if (src != null && navigator.onLine)
			{
				var remote = src.substring(0, 7) == 'http://' || src.substring(0, 8) == 'https://';
				
				if (remote && src.substring(0, converter.baseUrl.length) != converter.baseUrl &&
						(!self.crossOriginImages || !self.isCorsEnabledForUrl(src)))
				{
					src = PROXY_URL + '?url=' + encodeURIComponent(src);
				}
				else if (src.substring(0, 19) != 'chrome-extension://')
				{
					src = convert.apply(this, arguments);
				}
			}
			
			return src;
		};
		
		return converter;
	};

	/**
	 * 
	 */
	Editor.createSvgDataUri = function(svg)
	{
		return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
	};

	/**
	 * 
	 */
	Editor.prototype.convertImageToDataUri = function(url, callback, error, convertScale, forceConvert)
	{
		try
		{
			var acceptResponse = true;
			
			var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
			{
				acceptResponse = false;
				callback(url);
			}), this.timeout);

			// Fallback to raster image if SVG cannot be loaded
			var svgError = mxUtils.bind(this, function()
			{
				if (convertScale != null)
				{
					this.convertImageToDataUri(url, callback, error, convertScale, true);
				}
				else
				{
					callback(url);
				}
			});
	
			if (/(\.svg)$/i.test(url) && !forceConvert)
			{
				mxUtils.get(url, mxUtils.bind(this, function(req)
				{
			    	window.clearTimeout(timeoutThread);

					if (acceptResponse)
					{
						if (req.getStatus() < 200 || req.getStatus() > 299)
						{
							svgError();
						}
						else
						{
							callback(Editor.createSvgDataUri(req.getText()));
						}

						
					}
				}),
				mxUtils.bind(this, function()
				{
			    	window.clearTimeout(timeoutThread);

					if (acceptResponse)
					{
						svgError();
					}
				}));
			}
			else
			{
			    var img = new Image();
			    
			    if (this.crossOriginImages)
		    	{
				    img.crossOrigin = 'anonymous';
			    }
			    
			    img.onload = function()
			    {
			    	window.clearTimeout(timeoutThread);
					
					if (acceptResponse)
					{
				        try
				        {
							convertScale = (convertScale != null &&
								forceConvert) ? convertScale : 1;

					        var canvas = document.createElement('canvas');
					        canvas.height = img.height * convertScale;
					        canvas.width = img.width * convertScale;
					        var ctx = canvas.getContext('2d');
							ctx.scale(convertScale, convertScale);
					        ctx.drawImage(img, 0, 0);
							
				        	callback(canvas.toDataURL());
				        }
				        catch (e)
				        {
			        		callback(url);
				        }
					}
			    };
			    
			    img.onerror = function()
			    {
			    	window.clearTimeout(timeoutThread);
					
					if (acceptResponse)
					{
						if (error != null)
						{
							error();
						}
						else
						{
							callback(url);
						}
					}
			    };
			    
			    img.src = url;
			}
		}
		catch (e)
		{
			if (error != null)
			{
				error();
			}
			else
			{
				callback(url);
			}
		}
	};
	
	/**
	 * Converts all images in the SVG output to data URIs for immediate rendering
	 */
	Editor.prototype.convertImages = function(svgRoot, callback, imageCache, converter)
	{
		// Converts images to data URLs for immediate painting
		if (converter == null)
		{
			converter = this.createImageUrlConverter();
		}
		
		// Queues image conversion and executes in order
		var pending = [];

		function next()
		{
			if (pending.length > 0)
			{
				pending.shift()();
			}
			else
			{
				callback(svgRoot);
			}
		};

		var cache = imageCache || new Object();
		
		var convertImages = mxUtils.bind(this, function(tagName, srcAttr)
		{
			var images = svgRoot.getElementsByTagName(tagName);
			
			for (var i = 0; i < images.length; i++)
			{
				(mxUtils.bind(this, function(img)
				{
					pending.push(mxUtils.bind(this, function()
					{
						try
						{
							if (img != null)
							{
								var src = converter.convert(img.getAttribute(srcAttr));

								// Data URIs are pass-through
								if (src != null && src.substring(0, 5) != 'data:')
								{
									var tmp = cache[src];

									if (tmp == null)
									{
										this.convertImageToDataUri(src, function(uri)
										{
											if (uri != null)
											{
												cache[src] = uri;
												img.setAttribute(srcAttr, uri);
											}
											
											next();
										}, null, Editor.svgRasterScale);
									}
									else
									{
										img.setAttribute(srcAttr, tmp);

										next();
									}
								}
								else
								{
									if (src != null)
									{
										img.setAttribute(srcAttr, src);
									}

									next();
								}
							}
						}
						catch (e)
						{
							next();
						}
					}));
				}))(images[i]);
			}
		});
		
		// Converts all known image tags in output
		// LATER: Add support for images in CSS
		convertImages('image', 'xlink:href');
		convertImages('img', 'src');
		next();
	};
		
	/**
	 * Base64 encodes the given string. This method seems to be more
	 * robust for encoding PNG from binary AJAX responses.
	 */
	Editor.base64Encode = function(str)
	{
	    var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	    var out = "", i = 0, len = str.length, c1, c2, c3;
	    
	    while (i < len)
	    {
	        c1 = str.charCodeAt(i++) & 0xff;
	        
	        if (i == len)
	        {
	            out += CHARS.charAt(c1 >> 2);
	            out += CHARS.charAt((c1 & 0x3) << 4);
	            out += "==";
	            break;
	        }
	        
	        c2 = str.charCodeAt(i++);
	        
	        if (i == len)
	        {
	            out += CHARS.charAt(c1 >> 2);
	            out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
	            out += CHARS.charAt((c2 & 0xF) << 2);
	            out += "=";
	            break;
	        }
	        
	        c3 = str.charCodeAt(i++);
	        out += CHARS.charAt(c1 >> 2);
	        out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
	        out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
	        out += CHARS.charAt(c3 & 0x3F);
	    }
	    
	    return out;
	};

	/**
	 * Checks if the client is authorized and calls the next step.
	 */
	Editor.prototype.loadUrl = function(url, success, error, forceBinary, retry, dataUriPrefix, noBinary, headers)
	{
		try
		{
			var binary = !noBinary && (forceBinary || /(\.png)($|\?)/i.test(url) ||
				/(\.jpe?g)($|\?)/i.test(url) || /(\.gif)($|\?)/i.test(url) ||
				/(\.pdf)($|\?)/i.test(url));
			retry = (retry != null) ? retry : true;
			
			var fn = mxUtils.bind(this, function()
			{
				mxUtils.get(url, mxUtils.bind(this, function(req)
				{
					if (req.getStatus() >= 200 && req.getStatus() <= 299)
					{
				    	if (success != null)
				    	{
					    	var data = req.getText();
					    	
				    		// Returns PNG as base64 encoded data URI
							if (binary)
							{
								// LATER: Could be JPG but modern browsers
								// ignore the mime type in the data URI
								dataUriPrefix = (dataUriPrefix != null) ?
									dataUriPrefix : 'data:image/png;base64,';
								data = dataUriPrefix + Editor.base64Encode(data);
							}
							
				    		success(data);
				    	}
					}
					else if (error != null)
			    	{
						if (req.getStatus() == 0)
						{
							// Handles CORS errors
							error({message: mxResources.get('accessDenied')}, req);
						}
						else if (req.getStatus() == 404)
						{
							error({message: mxResources.get('fileNotFound'),
								code: req.getStatus()}, req);
						}
						else
						{
							error({message: this.getErrorMessage(req)}, req);
						}
			    	}
				}), function(req)
				{
			    	if (error != null)
			    	{
			    		error({message: mxResources.get('error') + ' ' + req.getStatus()});
			    	}
				}, binary, this.timeout, function()
			    {
				    if (retry && error != null)
					{
						error({code: App.ERROR_TIMEOUT, retry: fn});
					}
			    }, headers);
			});
			
			fn();
		}
		catch (e)
		{
			if (error != null)
			{
				error(e);
			}
		}
	};

	/**
	 * Adds the listener for automatically saving the diagram for local changes.
	 */
	Editor.prototype.getErrorMessage = function(req)
	{
		var msg = mxResources.get('error') + ' ' + req.getStatus();

		try
		{
			var data = req.getText();
			var obj = JSON.parse(data);

			if (obj != null && obj.error != null &&
				obj.error.message != null)
			{
				msg = obj.error.message + ' (' + msg + ')';
			}
		}
		catch (e)
		{
			// ignore
		}

		return msg;
	};

	/**
	 * Makes all relative font URLs absolute in the given font CSS.
	 */
    Editor.prototype.absoluteCssFonts = function(fontCss)
    {
    	var result = null;
    	
    	if (fontCss != null)
    	{
    		var parts = fontCss.split('url(');
    		
    		if (parts.length > 0)
    		{
    			result = [parts[0]];
    		
    			// Gets path for URL
    			var path = window.location.pathname;
    			var idx = (path != null) ? path.lastIndexOf('/') : -1;
    			
    			if (idx >= 0)
    			{
    				path = path.substring(0, idx + 1);
    			}
    			
    			// Gets base tag from head
    			var temp = document.getElementsByTagName('base');
    			var base = null;
    			
    			if (temp != null && temp.length > 0)
    			{
    				base = temp[0].getAttribute('href');
    			}
    		
    			for (var i = 1; i < parts.length; i++)
    			{
    				var idx = parts[i].indexOf(')');
    				
    				if (idx > 0)
    				{
	    				var url = Editor.trimCssUrl(parts[i].substring(0, idx));
	    				
	    				if (this.graph.isRelativeUrl(url))
	    				{
	                        url = (base != null) ? base + url : (window.location.protocol + '//' + window.location.hostname +
	                        	((url.charAt(0) == '/') ? '' : path) + url);
	                    }
	    				
	    				result.push('url("' + url + '"' + parts[i].substring(idx));
    				}
    				else
    				{
    					result.push(parts[i]);
    				}
    			}
    		}
    		else
    		{
    			result = [fontCss]
    		}
    	}
    	
    	return (result != null) ? result.join('') : null;
	};
	
	/**
	 * Returns the URL and mime type to be used for the given font.
	 */
	Editor.prototype.mapFontUrl = function(mime, url, fn)
	{
		if ((/^https?:\/\//.test(url)) && !this.isCorsEnabledForUrl(url))
		{
			url = PROXY_URL + '?url=' + encodeURIComponent(url);
		}

		fn(mime, url);
	};

	/**
	 * For the fonts in CSS to be applied when rendering images on canvas, the actual
	 * font data must be made available via a data URI encoding of the file.
	 */
    Editor.prototype.embedCssFonts = function(fontCss, then)
    {
        var parts = fontCss.split('url(');
        var waiting = 0;
		
        if (this.cachedFonts == null) 
        {
        	this.cachedFonts = {};
        }

        var finish = mxUtils.bind(this, function()
        {
            if (waiting == 0)
            {
                // Constructs string
                var result = [parts[0]];
                
                for (var j = 1; j < parts.length; j++)
                {
                    var idx = parts[j].indexOf(')');
                    result.push('url("');
                    result.push(this.cachedFonts[Editor.trimCssUrl(parts[j].substring(0, idx))]);
                    result.push('"' + parts[j].substring(idx));
                }
				
                then(result.join(''));
            }
        });
        
        if (parts.length > 0)
        {
            for (var i = 1; i < parts.length; i++)
            {
                var idx = parts[i].indexOf(')');
                var format = null;
                
                // Checks if there is a format directive
                var fmtIdx = parts[i].indexOf('format(', idx);
                
                if (fmtIdx > 0)
                {
                    format = Editor.trimCssUrl(parts[i].substring(fmtIdx + 7, parts[i].indexOf(')', fmtIdx)));
                }

                (mxUtils.bind(this, function(url)
                {
                    if (this.cachedFonts[url] == null)
                    {
                        // Mark font as being fetched and fetch it
                    	this.cachedFonts[url] = url;
                        waiting++;
                        
                        var mime = 'application/x-font-ttf';
                        
                        // See https://stackoverflow.com/questions/2871655/proper-mime-type-for-fonts
                        if (format == 'svg' || /(\.svg)($|\?)/i.test(url))
                        {
                            mime = 'image/svg+xml';
                        }
                        else if (format == 'otf' || format == 'embedded-opentype' || /(\.otf)($|\?)/i.test(url))
                        {
                            mime = 'application/x-font-opentype';
                        }
                        else if (format == 'woff' || /(\.woff)($|\?)/i.test(url))
                        {
                            mime = 'application/font-woff';
                        }
                        else if (format == 'woff2' || /(\.woff2)($|\?)/i.test(url))
                        {
                            mime = 'application/font-woff2';
                        }
                        else if (format == 'eot' || /(\.eot)($|\?)/i.test(url))
                        {
                            mime = 'application/vnd.ms-fontobject';
                        }
                        else if (format == 'sfnt' || /(\.sfnt)($|\?)/i.test(url))
                        {
                            mime = 'application/font-sfnt';
                        }

						this.mapFontUrl(mime, url, mxUtils.bind(this, function(realMime, realUrl)
						{
							// LATER: Remove cache-control header
							this.loadUrl(realUrl, mxUtils.bind(this, function(uri)
							{
								this.cachedFonts[url] = uri;
								waiting--;
								finish();
							}), mxUtils.bind(this, function(err)
							{
								// LATER: handle error
								waiting--;
								finish();
							}), true, null, 'data:' + realMime + ';charset=utf-8;base64,');
						}));
                    }
                }))(Editor.trimCssUrl(parts[i].substring(0, idx)), format);
            }
            
            //In case all fonts are cached
            finish();
        }
        else
    	{
        	//No font urls found
        	then(fontCss);
    	}
    };
	
	/**
	 * For the fontCSS to be applied when rendering images on canvas, the actual
	 * font data must be made available via a data URI encoding of the file.
	 */
    Editor.prototype.loadFonts = function(then)
    {
        if (this.fontCss != null && this.resolvedFontCss == null)
        {
        	this.embedCssFonts(this.fontCss, mxUtils.bind(this, function(resolvedFontCss)
			{
        		this.resolvedFontCss = resolvedFontCss;

				if (then != null)
				{
        			then();
				}
			}));
        }
        else if (then != null)
        {
            then();
        }
    };

	/**
	 * Returns a CSS mapping for the given CSS URL.
	 */
	Editor.prototype.createGoogleFontCache = function()
	{
		var cache = {};

		for (var key in Graph.fontMapping)
		{
			if (Graph.isCssFontUrl(key))
			{
				cache[key] = Graph.fontMapping[key];
			}
		}

		return cache;
	};
    
    /**
     * Embeds external fonts
     */
    Editor.prototype.embedExtFonts = function(callback)
    {
    	var extFonts = this.graph.getCustomFonts();
		
		if (extFonts.length > 0)
		{
			var content = [];
			var waiting = 0;
			
			if (this.cachedGoogleFonts == null)
			{
				this.cachedGoogleFonts = this.createGoogleFontCache();
			}
			
			var googleCssDone = mxUtils.bind(this, function()
			{
				if (waiting == 0)
	            {
					this.embedCssFonts(content.join(''), callback);
	            }
			});
			
			for (var i = 0; i < extFonts.length; i++)
			{
				(mxUtils.bind(this, function(fontName, fontUrl)
				{
					if (Graph.isCssFontUrl(fontUrl))
					{
						if (this.cachedGoogleFonts[fontUrl] == null)
						{
							waiting++;
							
							this.loadUrl(Graph.rewriteGoogleFontUrl(fontUrl), mxUtils.bind(this, function(css)
		                    {
								this.cachedGoogleFonts[fontUrl] = css;
								content.push(css + '\n');
		                        waiting--;
		                        googleCssDone();
		                    }), mxUtils.bind(this, function(err)
		                    {
		                        waiting--;
								content.push('@import url(' + fontUrl + ');\n');
		                        googleCssDone();
		                    }));
						}
						else
						{
							content.push(this.cachedGoogleFonts[fontUrl] + '\n');
						}
					}
					else
					{
						content.push('@font-face {' +
							'font-family: "' + fontName + '";' + 
							'src: url("' + fontUrl + '")}\n');
					}
				}))(extFonts[i].name, extFonts[i].url);
			}
			
			googleCssDone();
		}
		else
		{
			callback();
		}
    };
	
	/**
	 * Copies MathJax CSS into the SVG output.
	 */
	Editor.prototype.addMathCss = function(svgRoot)
	{
		var defs = svgRoot.getElementsByTagName('defs');
		
		if (defs != null && defs.length > 0)
		{
			var styles = document.getElementsByTagName('style');
			
			for (var i = 0; i < styles.length; i++)
			{
				// Ignores style elements with no MathJax CSS
				var content = mxUtils.getTextContent(styles[i]);

				if (content.indexOf('mxPageSelector') < 0 &&
					content.indexOf('MathJax') > 0)
				{
					defs[0].appendChild(styles[i].cloneNode(true));
				}
			}
		}
	};

	/**
	 * Adds the global fontCss configuration.
	 */
	Editor.prototype.addFontCss = function(svgRoot, fontCss)
	{
		fontCss = (fontCss != null) ? fontCss : this.absoluteCssFonts(this.fontCss);

		// Creates defs element if not available
		if (fontCss != null)
		{
			var defsElt = mxUtils.getSvgDefs(svgRoot);

			// Moves imports to separate style element
			var lines = fontCss.split('\n');
			var imports = [];
			var other = [];

			for (var i = 0; i < lines.length; i++)
			{
				if (lines[i].substring(0, 7) == '@import')
				{
					imports.push(lines[i]);
				}
				else
				{
					other.push(lines[i]);
				}
			}
			
			var svgDoc = svgRoot.ownerDocument;
			var style = (svgDoc.createElementNS != null) ?
				svgDoc.createElementNS(mxConstants.NS_SVG, 'style') :
				svgDoc.createElement('style');
			style.setAttribute('type', 'text/css');

			if (imports.length > 0)
			{
				mxUtils.setTextContent(style, imports.join('\n'));
				defsElt.appendChild(style);
			}

			if (other.length > 0)
			{
				style = style.cloneNode(false);
				mxUtils.setTextContent(style, other.join('\n'));
				defsElt.appendChild(style);
			}
		}
	};
	
	/**
	 * Disables client-side image export if math is enabled.
	 */
	Editor.prototype.isExportToCanvas = function()
	{
		return mxClient.IS_CHROMEAPP || Editor.useCanvasForExport;
	};

	/**
	 * Returns the maximum possible scale for the given canvas dimension and scale.
	 * This will return the given scale or the maximum scale that can be used to
	 * generate a valid image in the current browser.
	 * 
	 * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas
	 */
	Editor.prototype.getMaxCanvasScale = function(w, h, scale)
	{
		var max = (mxClient.IS_FF) ? 8192 : 16384;

		return Math.min(scale, Math.min(max / w, max / h));
	};
	
	/**
	 *
	 */
	Editor.prototype.exportToCanvas = function(callback, width, imageCache, background, error, limitHeight,
		ignoreSelection, scale, transparentBackground, addShadow, converter, graph, border, noCrop, grid,
		theme, exportType, cells)
	{
		try
		{
			limitHeight = (limitHeight != null) ? limitHeight : true;
			ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
			graph = (graph != null) ? graph : this.graph;
			border = (border != null) ? border : 0;
			
			var bg = (transparentBackground) ? null : graph.background;
			
			if (bg == mxConstants.NONE)
			{
				bg = null;
			}
			
			if (bg == null)
			{
				bg = background;
			}
			
			// Handles special case where background is null but transparent is false
			if (bg == null && transparentBackground == false)
			{
				bg = 'light-dark(#ffffff,' + Editor.darkColor + ')';
			}

			this.convertImages(graph.getSvg(null, null, border, noCrop, null, ignoreSelection,
				null, null, null, addShadow, null, theme, exportType, cells),
				mxUtils.bind(this, function(svgRoot)
			{
				try
				{
					svgRoot.style.setProperty('-webkit-font-smoothing', 'antialiased');
					svgRoot.style.setProperty('-moz-osx-font-smoothing', 'grayscale');
					
					var img = new Image();
					
					img.onload = mxUtils.bind(this, function()
					{
				   		try
				   		{
				   			var canvas = document.createElement('canvas');
							var w = parseInt(svgRoot.getAttribute('width'));
							var h = parseInt(svgRoot.getAttribute('height'));
							scale = (scale != null) ? scale : 1;
							
							if (width != null)
							{
								scale = (!limitHeight) ? width / w : Math.min(1, Math.min((width * 3) / (h * 4), width / w));
							}
							
							scale = this.getMaxCanvasScale(w, h, scale);
							w = Math.ceil(scale * w);
							h = Math.ceil(scale * h);
							
							canvas.setAttribute('width', w);
					   		canvas.setAttribute('height', h);
					   		var ctx = canvas.getContext('2d');
					   		
					   		if (bg != null)
					   		{
								var cssBg = mxUtils.getLightDarkColor(bg);
					   			ctx.beginPath();
								ctx.rect(0, 0, w, h);
								ctx.fillStyle = (theme == 'dark' &&
									graph.getAdaptiveColors() != 'none') ?
									cssBg.dark : cssBg.light;
								ctx.fill();
					   		}
		
					   		if (scale != 1)
					   		{
					   			ctx.scale(scale, scale);
					   		}
	
						    function drawImage()
						    {
						    	// Workaround for broken data URI images in Safari on first export
						   		if (mxClient.IS_SF)
						   		{			   		
									window.setTimeout(function()
									{
										ctx.drawImage(img, 0, 0);
										callback(canvas, svgRoot);
									}, 0);
						   		}
						   		else
						   		{
						   			ctx.drawImage(img, 0, 0);
						   			callback(canvas, svgRoot);
						   		}
						    };
						    
						    if (grid)
						    {
							    var view = graph.view;
							    var curViewScale = view.scale;
							    view.scale = 1; //Reset the scale temporary to generate unscaled grid image which is then scaled
								var gridImage = btoa(unescape(encodeURIComponent(view.createSvgGrid(view.gridColor))));
								view.scale = curViewScale;
								gridImage = 'data:image/svg+xml;base64,' + gridImage;
				                var phase = graph.gridSize * view.gridSteps * scale;
				                
				                var b = graph.getGraphBounds();
								var tx = view.translate.x * curViewScale;
								var ty = view.translate.y * curViewScale;
								var x0 = tx + (b.x - tx) / curViewScale - border;
								var y0 = ty + (b.y - ty) / curViewScale - border;
								
								var background = new Image();
		
								background.onload = function()
								{
									try
									{
										var x = -Math.round(phase - mxUtils.mod((tx - x0) * scale, phase));
										var y = -Math.round(phase - mxUtils.mod((ty - y0) * scale, phase));
			
										for (var i = x; i < w; i += phase)
										{
											for (var j = y; j < h; j += phase)
											{
												ctx.drawImage(background, i / scale, j / scale);	
											}
										}
									
										drawImage();
									}
							   		catch (e)
							   		{
							   			if (error != null)
										{
											error(e);
										}
							   		}
								};
								
								background.onerror = function(e)
								{
									if (error != null)
									{
										error(e);
									}
								};
								
								background.src = gridImage;
						    }
						    else
					    	{
						    	drawImage();
					    	}
				   		}
				   		catch (e)
				   		{
				   			if (error != null)
							{
								error(e);
							}
				   		}
					});
					
					img.onerror = function(e)
					{
						//console.log('img', e, img.src);
						
						if (error != null)
						{
							error(e);
						}
					};

					if (addShadow)
					{
						this.graph.addSvgShadow(svgRoot);
					}
					
					if (this.graph.mathEnabled)
					{
						this.addMathCss(svgRoot);
					}
					
					var done = mxUtils.bind(this, function()
					{
						try
						{
							if (this.resolvedFontCss != null)
							{
								this.addFontCss(svgRoot, this.resolvedFontCss);
							}
							
							img.src = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
						}
						catch (e)
						{
							if (error != null)
							{
								error(e);
							}
						}
					});
					
					this.embedExtFonts(mxUtils.bind(this, function(extFontsEmbeddedCss)
					{
						try
						{
							if (extFontsEmbeddedCss != null)
							{
								this.addFontCss(svgRoot, extFontsEmbeddedCss);
							}
							
							this.loadFonts(done);
						}
						catch (e)
						{
							if (error != null)
							{
								error(e);
							}
						}
					}));
				}
				catch (e)
				{
					//console.log('src', e, img.src);
					
					if (error != null)
					{
						error(e);
					}
				}
			}), imageCache, converter);
		}
		catch (e)
		{
			if (error != null)
			{
				error(e);
			}
		}
	};
	
	Editor.crcTable = [];
	
	for (var n = 0; n < 256; n++)
	{
		var c = n;
		
		for (var k = 0; k < 8; k++)
		{
			if ((c & 1) == 1)
			{
				c = 0xedb88320 ^ (c >>> 1);
			}
			else
			{
				c >>>= 1;
			}

			Editor.crcTable[n] = c;
		}
	}
	
	Editor.updateCRC = function(crc, data, off, len)
	{
		var c = crc;
	
		for (var n = 0; n < len; n++)
		{
			c = Editor.crcTable[(c ^ data.charCodeAt(off + n)) & 0xff] ^ (c >>> 8);
		}
	
		return c;
	};

	Editor.crc32 = function(str)
	{
	    var crc = 0 ^ (-1);

	    for (var i = 0; i < str.length; i++ )
	    {
	        crc = (crc >>> 8) ^ Editor.crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
	    }

	    return (crc ^ (-1)) >>> 0;
	};

	/**
	 * Adds the given text to the compressed or non-compressed text chunk.
	 */
	Editor.writeGraphModelToPng = function(data, type, key, value, error)
	{
		var base64 = data.substring(data.indexOf(',') + 1);
		var f = (window.atob) ? atob(base64) : Base64.decode(base64, true);
		var pos = 0;
		
		function fread(d, count)
		{
			var start = pos;
			pos += count;
			
			return d.substring(start, pos);
		};
		
		// Reads unsigned long 32 bit big endian
		function _freadint(d)
		{
			var bytes = fread(d, 4);
			
			return bytes.charCodeAt(3) + (bytes.charCodeAt(2) << 8) +
				(bytes.charCodeAt(1) << 16) + (bytes.charCodeAt(0) << 24);
		};
		
		function writeInt(num)
		{
			return String.fromCharCode((num >> 24) & 0x000000ff, (num >> 16) & 0x000000ff,
				(num >> 8) & 0x000000ff, num & 0x000000ff);
		};
		
		// Checks signature
		if (fread(f,8) != String.fromCharCode(137) + 'PNG' + String.fromCharCode(13, 10, 26, 10))
		{
			if (error != null)
			{
				error();
			}
			
			return;
		}
		
		// Reads header chunk
		fread(f,4);
		
		if (fread(f,4) != 'IHDR')
		{
			if (error != null)
			{
				error();
			}
			
			return;
		}
		
		fread(f, 17);
		var result = f.substring(0, pos);
		
		do
		{
			var n = _freadint(f);
			var chunk = fread(f,4);
			
			if (chunk == 'IDAT')
			{
				result = f.substring(0, pos - 8);
				
				if (type == 'pHYs' && key == 'dpi')
				{
					var dpm = Math.round(value / 0.0254); //One inch is equal to exactly 0.0254 meters.
					var chunkData = writeInt(dpm) + writeInt(dpm) + String.fromCharCode(1);
				}
				else
				{
					var chunkData = key + String.fromCharCode(0) +
						((type == 'zTXt') ? String.fromCharCode(0) : '') + 
						value;
				}
				
				var crc = 0xffffffff;
				crc = Editor.updateCRC(crc, type, 0, 4);
				crc = Editor.updateCRC(crc, chunkData, 0, chunkData.length);
				
				result += writeInt(chunkData.length) + type + chunkData + writeInt(crc ^ 0xffffffff);
				result += f.substring(pos - 8, f.length);
				
				break;
			}
			
			result += f.substring(pos - 8, pos - 4 + n);
			fread(f,n);
			fread(f,4);
		}
		while (n);
		
		return 'data:image/png;base64,' + ((window.btoa) ? btoa(result) : Base64.encode(result, true));
	};

	/**
	 * Adds persistence for recent colors
	 */
	if (window.ColorDialog)
	{
		FilenameDialog.filenameHelpLink = 'https://www.drawio.com/doc/faq/save-file-formats'; 
		
		var colorDialogAddRecentColor = ColorDialog.addRecentColor;
		
		ColorDialog.addRecentColor = function(color, max)
		{
			colorDialogAddRecentColor.apply(this, arguments);
			
			mxSettings.setRecentColors(ColorDialog.recentColors);
			mxSettings.save();
		};
		
		var colorDialogResetRecentColors = ColorDialog.resetRecentColors;
		
		ColorDialog.resetRecentColors = function()
		{
			colorDialogResetRecentColors.apply(this, arguments);
			
			mxSettings.setRecentColors(ColorDialog.recentColors);
			mxSettings.save();
		};
	}
	
	// Overrides ID for pages
	if (typeof window.EditDataDialog !== 'undefined')
	{
		EditDataDialog.getDisplayIdForCell = function(ui, cell, optionalGraph)
		{
			var id = null;

			if (optionalGraph != null)
			{
				return null;
			}

			if (ui.editor.graph.getModel().getParent(cell) != null)
			{
				id = cell.getId();
			}
			else if (ui.currentPage != null)
			{
				id = ui.currentPage.getId();
			}

			return id;
		};
	}

	var AddCustomPropertyDialog = function(editorUi, callback)
	{
		var row, td;
		
		var table = document.createElement('table');
		var tbody = document.createElement('tbody');
		table.setAttribute('cellpadding', (mxClient.IS_SF) ? '0' : '2');
		
		row = document.createElement('tr');
		
		td = document.createElement('td');
		td.style.fontSize = '10pt';
		td.style.width = '100px';
		mxUtils.write(td, mxResources.get('name', null, 'Name') + ':');
		
		row.appendChild(td);
		
		var nameInput = document.createElement('input');
		nameInput.style.width = '180px';

		td = document.createElement('td');
		td.appendChild(nameInput);
		row.appendChild(td);
		
		tbody.appendChild(row);
			
		row = document.createElement('tr');
		
		td = document.createElement('td');
		td.style.fontSize = '10pt';
		mxUtils.write(td, mxResources.get('type', null, 'Type') + ':');
		
		row.appendChild(td);
		
		var typeSelect = document.createElement('select');
		typeSelect.style.width = '180px';

		var boolOption = document.createElement('option');
		boolOption.setAttribute('value', 'bool');
		mxUtils.write(boolOption, mxResources.get('bool', null, 'Boolean'));
		typeSelect.appendChild(boolOption);
		
		var clrOption = document.createElement('option');
		clrOption.setAttribute('value', 'color');
		mxUtils.write(clrOption, mxResources.get('color', null, 'Color'));
		typeSelect.appendChild(clrOption);
		
		var enumOption = document.createElement('option');
		enumOption.setAttribute('value', 'enum');
		mxUtils.write(enumOption, mxResources.get('enum', null, 'Enumeration'));
		typeSelect.appendChild(enumOption);

		var floatOption = document.createElement('option');
		floatOption.setAttribute('value', 'float');
		mxUtils.write(floatOption, mxResources.get('float', null, 'Float'));
		typeSelect.appendChild(floatOption);

		var intOption = document.createElement('option');
		intOption.setAttribute('value', 'int');
		mxUtils.write(intOption, mxResources.get('int', null, 'Int'));
		typeSelect.appendChild(intOption);
		
		var strOption = document.createElement('option');
		strOption.setAttribute('value', 'string');
		mxUtils.write(strOption, mxResources.get('string', null, 'String'));
		typeSelect.appendChild(strOption);

		td = document.createElement('td');
		td.appendChild(typeSelect);
		row.appendChild(td);
		
		tbody.appendChild(row);
		
		row = document.createElement('tr');

		td = document.createElement('td');
		td.style.fontSize = '10pt';
		mxUtils.write(td, mxResources.get('dispName', null, 'Display Name') + ':');
		
		row.appendChild(td);
		
		var dispNameInput = document.createElement('input');
		dispNameInput.style.width = '180px';

		td = document.createElement('td');
		td.appendChild(dispNameInput);
		row.appendChild(td);

		tbody.appendChild(row);

		var listRow = document.createElement('tr');

		td = document.createElement('td');
		td.style.fontSize = '10pt';
		mxUtils.write(td, mxResources.get('enumList', null, 'Enum List') + ' (csv):');
		
		listRow.appendChild(td);
		
		var enumListInput = document.createElement('input');
		enumListInput.style.width = '180px';

		td = document.createElement('td');
		td.appendChild(enumListInput);
		listRow.appendChild(td);

		listRow.style.display = 'none';
		tbody.appendChild(listRow);
		
		table.appendChild(tbody);
		
		function typeChanged()
		{
			if (typeSelect.value === 'enum')
			{
				listRow.style.display = '';
				this.container.parentNode.style.height = "150px";
				
			}
			else
			{
				listRow.style.display = 'none';
				this.container.parentNode.style.height = "130px";
			}
		};
		
		mxEvent.addListener(typeSelect, 'change', mxUtils.bind(this, typeChanged));

		row = document.createElement('tr');
		td = document.createElement('td');
		td.setAttribute('align', 'right');
		td.style.paddingTop = '22px';
		td.colSpan = 2;
		
		var addBtn = mxUtils.button(mxResources.get('add', null, 'Add'), mxUtils.bind(this, function()
		{
	    	var name = nameInput.value;

	    	if (name == "")
    		{
	    		nameInput.style.border = "1px solid red";
	    		return;
    		}
	    	
			var type = typeSelect.value;
	    	var dispName = dispNameInput.value;

	    	if (dispName == "")
    		{
	    		dispNameInput.style.border = "1px solid red";
	    		return;
    		}

	    	var enumList = enumListInput.value;
			
	    	if (enumList == "" && type == "enum")
    		{
	    		enumListInput.style.border = "1px solid red";
	    		return;
	    		
    		}
	    	
			if (enumList != null)
			{
				enumList = enumList.split(',');
				
				for (var i = 0; i < enumList.length; i++)
				{
					enumList[i] = enumList[i].trim();
				}
			}
			
			if (callback)
			{
				callback(editorUi, name, type, dispName, enumList);
				editorUi.hideDialog();
			}
		}));
		addBtn.className = 'geBtn gePrimaryBtn';
		
		var cancelBtn = mxUtils.button(mxResources.get('cancel'), function()
		{
			editorUi.hideDialog();
		});
		cancelBtn.className = 'geBtn';
		
		if (editorUi.editor.cancelFirst)
		{
			td.appendChild(cancelBtn);
			td.appendChild(addBtn);
		}
		else
		{
			td.appendChild(addBtn);
			td.appendChild(cancelBtn);
		}

		row.appendChild(td);
		tbody.appendChild(row);
		table.appendChild(tbody);
		this.container = table;
	};
	
	if (window.StyleFormatPanel != null)
	{
		var formatInit = Format.prototype.init;
		
		Format.prototype.init = function()
		{
			formatInit.apply(this, arguments);

			this.editorUi.editor.addListener('fileLoaded', this.update);
		};

		var formatRefresh = Format.prototype.refresh;
		
		Format.prototype.refresh = function()
		{
			if (this.editorUi.getCurrentFile() != null || urlParams['embed'] == '1' ||
				this.editorUi.editor.chromeless)
			{
				formatRefresh.apply(this, arguments);
			}
			else
			{
				this.clear();
			}
		};

		/**
		 * Option is not visible in default theme.
		 */
	    DiagramFormatPanel.prototype.isMathOptionVisible = function(div)
	    {
			return (Editor.currentTheme == 'simple' ||
				Editor.currentTheme == 'sketch' ||
				Editor.currentTheme == 'min');
	    };
	    
		/**
		 * Adds autosave and math typesetting options.
		 */
		var diagramFormatPanelAddOptions = DiagramFormatPanel.prototype.addOptions;

		DiagramFormatPanel.prototype.addOptions = function(div)
		{
			div = diagramFormatPanelAddOptions.apply(this, arguments);
			
			var ui = this.editorUi;
			var editor = ui.editor;
			var graph = editor.graph;

			// Adds autosave option
			if (graph.isEnabled())
			{
				var file = ui.getCurrentFile();

				if (file != null && file.isAutosaveOptional())
				{
					div.appendChild(this.createOption(mxResources.get('autosave'), function()
					{
						return ui.editor.autosave;
					}, function(checked)
					{
						ui.editor.setAutosave(checked);
						
						if (ui.editor.autosave && file.isModified())
						{
							file.fileChanged();
						}
					},
					{
						install: function(apply)
						{
							this.listener = function()
							{
								apply(ui.editor.autosave);
							};
							
							ui.editor.addListener('autosaveChanged', this.listener);
						},
						destroy: function()
						{
							ui.editor.removeListener(this.listener);
						}
					}));
				}
			}
			
			// Adds math option
	        if (this.isMathOptionVisible() && graph.isEnabled() && typeof(MathJax) !== 'undefined')
	        {
				var option = this.createOption(mxResources.get('mathematicalTypesetting'), function()
				{
					return graph.mathEnabled;
				}, function(checked)
				{
					ui.actions.get('mathematicalTypesetting').funct();
				},
				{
					install: function(apply)
					{
						this.listener = function()
						{
							apply(graph.mathEnabled);
						};
						
						ui.addListener('mathEnabledChanged', this.listener);
					},
					destroy: function()
					{
						ui.removeListener(this.listener);
					}
				});
				
				div.appendChild(option);
				
				if (!ui.isOffline() || mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
				{
					option.appendChild(ui.menus.createHelpLink(
						'https://www.drawio.com/doc/faq/math-typesetting'));
				}
			}
			
			return div;
		};

		mxCellRenderer.prototype.defaultVertexShape.prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'absoluteArcSize', dispName: 'Abs. Arc Size', type: 'bool', defVal: false}
	      ];

		mxCellRenderer.defaultShapes['link'].prototype.customProperties = [
	        {name: 'width', dispName: 'Width', type: 'float', min:0, defVal: 4}
		];

		mxCellRenderer.defaultShapes['zigzag'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:5, defVal: 20}
		];

		mxCellRenderer.defaultShapes['flexArrow'].prototype.customProperties = [
	        {name: 'width', dispName: 'Width', type: 'float', min:0, defVal: 10},
	        {name: 'startWidth', dispName: 'Start Width', type: 'float', min:0, defVal: 20},
	        {name: 'endWidth', dispName: 'End Width', type: 'float', min:0, defVal: 20}
		];

		mxCellRenderer.defaultShapes['process'].prototype.customProperties = [
			{name: 'size', dispName: 'Indent', type: 'float', min: 0, max: 0.5, defVal: 0.1}
		];

		mxCellRenderer.defaultShapes['rhombus'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, max: 50, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'double', dispName: 'Double', type: 'bool', defVal: false}
		];
		
		mxCellRenderer.defaultShapes['partialRectangle'].prototype.customProperties = [
	        {name: 'top', dispName: 'Top Line', type: 'bool', defVal: true},
	        {name: 'bottom', dispName: 'Bottom Line', type: 'bool', defVal: true},
	        {name: 'left', dispName: 'Left Line', type: 'bool', defVal: true},
	        {name: 'right', dispName: 'Right Line', type: 'bool', defVal: true}
        ];
		
		mxCellRenderer.defaultShapes['parallelogram'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
			{name: 'size', dispName: 'Slope Angle', type: 'float', min:0, max: 1, defVal: 0.2, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '0';
			}},
	        {name: 'size', dispName: 'Slope Angle', type: 'float', min:0, defVal: 20, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '1';
			}}
		];
		
		mxCellRenderer.defaultShapes['hexagon'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
			{name: 'size', dispName: 'Slope Angle', type: 'float', min:0, max: 1, defVal: 0.25, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '0';
			}},
	        {name: 'size', dispName: 'Slope Angle', type: 'float', min:0, defVal: 25, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '1';
			}}
		];
		
		mxCellRenderer.defaultShapes['triangle'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE}
		];
		
		mxCellRenderer.defaultShapes['document'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', defVal: 0.3, min:0, max:1}
		];
		
		mxCellRenderer.defaultShapes['internalStorage'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'dx', dispName: 'Left Line', type: 'float', min:0, defVal: 20},
	        {name: 'dy', dispName: 'Top Line', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['cube'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:20 },
	        {name: 'darkOpacity', dispName: 'Dark Opacity', type: 'float', min:-1, max:1, defVal:0 },
	        {name: 'darkOpacity2', dispName: 'Dark Opacity 2', type: 'float', min:-1, max:1, defVal:0 }
		];
		
		mxCellRenderer.defaultShapes['step'].prototype.customProperties = [
			{name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'size', dispName: 'Notch Size', type: 'float', min:0, defVal:20},
	        {name: 'fixedSize', dispName: 'Fixed Size', type: 'bool', defVal:true}
		];
		
		mxCellRenderer.defaultShapes['trapezoid'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'size', dispName: 'Slope Angle', type: 'float', min:0, max: 1, defVal: 0.2}
		];
		
		mxCellRenderer.defaultShapes['tape'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, max:1, defVal:0.4 }
		];
		
		mxCellRenderer.defaultShapes['note'].prototype.customProperties = [
	        {name: 'size', dispName: 'Fold Size', type: 'float', min:0, defVal: 30},
	        {name: 'darkOpacity', dispName: 'Dark Opacity', type: 'float', min:-1, max:1, defVal:0 },
	    ];
		
		mxCellRenderer.defaultShapes['card'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'size', dispName: 'Cutoff Size', type: 'float', min:0, defVal: 30}
	    ];
		
		mxCellRenderer.defaultShapes['callout'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'base', dispName: 'Callout Width', type: 'float', min:0, defVal: 20},
	        {name: 'size', dispName: 'Callout Length', type: 'float', min:0, defVal: 30},
	        {name: 'position', dispName: 'Callout Position', type: 'float', min:0, max:1, defVal: 0.5},
	        {name: 'position2', dispName: 'Callout Tip Position', type: 'float', min:0, max:1, defVal: 0.5}
	    ];
		
		mxCellRenderer.defaultShapes['folder'].prototype.customProperties = [
	        {name: 'tabWidth', dispName: 'Tab Width', type: 'float'},
	        {name: 'tabHeight', dispName: 'Tab Height', type: 'float'},
	        {name: 'tabPosition', dispName: 'Tap Position', type: 'enum',
	        	enumList: [{val: 'left', dispName: 'Left'}, {val: 'right', dispName: 'Right'}]
	        }
	    ];
		
		mxCellRenderer.defaultShapes['swimlane'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 15},
	        {name: 'absoluteArcSize', dispName: 'Abs. Arc Size', type: 'bool', defVal: false},
	        {name: 'startSize', dispName: 'Header Size', type: 'float'},
			{name: 'swimlaneHead', dispName: 'Head Border', type: 'bool', defVal: true},
			{name: 'swimlaneBody', dispName: 'Body Border', type: 'bool', defVal: true},
	        {name: 'horizontal', dispName: 'Horizontal', type: 'bool', defVal: true},
	        {name: 'separatorColor', dispName: 'Separator Color', type: 'color', defVal: null},
	        {name: 'fixedHeader', dispName: 'Fixed Header', type: 'bool', defVal: true,
				getDefaultValue: function(state, format)
				{
					var shape = mxCellRenderer.defaultShapes[mxUtils.getValue(state.style, 'shape', '')];
					return (shape != null && shape.prototype.fixedHeaderDefault != null) ?
						(shape.prototype.fixedHeaderDefault ? '1' : '0') : '1';
				}},
	    ];

		mxCellRenderer.defaultShapes['table'].prototype.customProperties = [
			{name: 'rowLines', dispName: 'Row Lines', type: 'bool', defVal: true},
			{name: 'columnLines', dispName: 'Column Lines', type: 'bool', defVal: true},
			{name: 'fixedRows', dispName: 'Fixed Rows', type: 'bool', defVal: false},
			{name: 'resizeLast', dispName: 'Resize Last Column', type: 'bool', defVal: false},
			{name: 'resizeLastRow', dispName: 'Resize Last Row', type: 'bool', defVal: false}].
			concat(mxCellRenderer.defaultShapes['swimlane'].prototype.customProperties).
			concat(mxCellRenderer.defaultShapes['partialRectangle'].prototype.customProperties);

		mxCellRenderer.defaultShapes['tableRow'].prototype.customProperties =
			mxCellRenderer.defaultShapes['swimlane'].prototype.customProperties.
			concat(mxCellRenderer.defaultShapes['partialRectangle'].prototype.customProperties);
		
		mxCellRenderer.defaultShapes['doubleEllipse'].prototype.customProperties = [
	        {name: 'margin', dispName: 'Indent', type: 'float', min:0, defVal:4}
	    ];
		
		mxCellRenderer.defaultShapes['ext'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 15},
			{name: 'double', dispName: 'Double', type: 'bool', defVal: false},
	        {name: 'margin', dispName: 'Indent', type: 'float', min: 0, defVal:0}
	    ];
		
		mxCellRenderer.defaultShapes['curlyBracket'].prototype.customProperties = [
			{name: 'rounded', dispName: 'Rounded', type: 'bool', defVal: true},
	        {name: 'size', dispName: 'Size', type: 'float', min:0, max: 1, defVal: 0.5}
	    ];
		
		mxCellRenderer.defaultShapes['image'].prototype.customProperties = [
			{name: 'imageAspect', dispName: 'Fixed Image Aspect', type: 'bool', defVal:true}
	    ];
		
		mxCellRenderer.defaultShapes['label'].prototype.customProperties = [
			{name: 'imageAspect', dispName: 'Fixed Image Aspect', type: 'bool', defVal:true},
			{name: 'imageAlign', dispName: 'Image Align', type: 'enum',
				enumList: [{val: 'left', dispName: 'Left'}, 
						   {val: 'center', dispName: 'Center'}, 
						   {val: 'right', dispName: 'Right'}], defVal: 'left'},
			{name: 'imageVerticalAlign', dispName: 'Image Vertical Align', type: 'enum',
				enumList: [{val: 'top', dispName: 'Top'}, 
					       {val: 'middle', dispName: 'Middle'}, 
					       {val: 'bottom', dispName: 'Bottom'}], defVal: 'middle'},
	        {name: 'imageWidth', dispName: 'Image Width', type: 'float', min:0, defVal: 24},
	        {name: 'imageHeight', dispName: 'Image Height', type: 'float', min:0, defVal: 24},
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 12},
	        {name: 'absoluteArcSize', dispName: 'Abs. Arc Size', type: 'bool', defVal: false}
	    ];
		
		mxCellRenderer.defaultShapes['dataStorage'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, max:1, defVal:0.1 }
		];
		
		mxCellRenderer.defaultShapes['manualInput'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:30 },
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['loopLimit'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:20 },
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['offPageConnector'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:38 },
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['display'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min: 0, max: 1, defVal: 0.25 }
		];
		
		mxCellRenderer.defaultShapes['singleArrow'].prototype.customProperties = [
	        {name: 'arrowWidth', dispName: 'Arrow Width', type: 'float', min: 0, max: 1, defVal: 0.3 },
	        {name: 'arrowSize', dispName: 'Arrowhead Length', type: 'float', min: 0, max: 1, defVal: 0.2 }
		];
		
		mxCellRenderer.defaultShapes['doubleArrow'].prototype.customProperties = [
	        {name: 'arrowWidth', dispName: 'Arrow Width', type: 'float', min: 0, max: 1, defVal: 0.3 },
	        {name: 'arrowSize', dispName: 'Arrowhead Length', type: 'float', min: 0, max: 1, defVal: 0.2 }
		];
		
		mxCellRenderer.defaultShapes['cross'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min: 0, max: 1, defVal: 0.2 }
		];
		
		mxCellRenderer.defaultShapes['corner'].prototype.customProperties = [
	        {name: 'dx', dispName: 'Width1', type: 'float', min: 0, defVal: 20 },
	        {name: 'dy', dispName: 'Width2', type: 'float', min: 0, defVal: 20 }
		];
		
		mxCellRenderer.defaultShapes['tee'].prototype.customProperties = [
	        {name: 'dx', dispName: 'Width1', type: 'float', min: 0, defVal: 20 },
	        {name: 'dy', dispName: 'Width2', type: 'float', min: 0, defVal: 20 }
		];
		
		mxCellRenderer.defaultShapes['umlLifeline'].prototype.customProperties = [
			{name: 'participant', dispName:'Participant', type:'enum', defVal:'none', enumList:[
				{val:'none', dispName: 'Default'},	
				{val:'umlActor', dispName: 'Actor'},	
				{val:'umlBoundary', dispName: 'Boundary'},	
				{val:'umlEntity', dispName: 'Entity'},	
				{val:'umlControl', dispName: 'Control'},	
				]},
			{name: 'size', dispName:'Height', type:'float', defVal:40, min:0}
		];
		
		mxCellRenderer.defaultShapes['umlFrame'].prototype.customProperties = [
			{name: 'width', dispName:'Title Width', type:'float', defVal:60, min:0},
			{name: 'height', dispName:'Title Height', type:'float', defVal:30, min:0}
		];
		
		/**
		 * Configures global color schemes.
		 */
		StyleFormatPanel.prototype.defaultColorSchemes = [[{fill: '', stroke: ''}, {fill: '#f5f5f5', stroke: '#666666', font: '#333333'},
			{fill: '#dae8fc', stroke: '#6c8ebf'}, {fill: '#d5e8d4', stroke: '#82b366'},
			{fill: '#ffe6cc', stroke: '#d79b00'}, {fill: '#fff2cc', stroke: '#d6b656'},
			{fill: '#f8cecc', stroke: '#b85450'}, {fill: '#e1d5e7', stroke: '#9673a6'}],
			[{fill: '', stroke: ''}, {fill: '#60a917', stroke: '#2D7600', font: '#ffffff'},
			{fill: '#008a00', stroke: '#005700', font: '#ffffff'}, {fill: '#1ba1e2', stroke: '#006EAF', font: '#ffffff'},
			{fill: '#0050ef', stroke: '#001DBC', font: '#ffffff'}, {fill: '#6a00ff', stroke: '#3700CC', font: '#ffffff'},
			//{fill: '#aa00ff', stroke: '#7700CC', font: '#ffffff'},
			{fill: '#d80073', stroke: '#A50040', font: '#ffffff'}, {fill: '#a20025', stroke: '#6F0000', font: '#ffffff'}],
			[{fill: '#e51400', stroke: '#B20000', font: '#ffffff'}, {fill: '#fa6800', stroke: '#C73500', font: '#000000'},
			{fill: '#f0a30a', stroke: '#BD7000', font: '#000000'}, {fill: '#e3c800', stroke: '#B09500', font: '#000000'},
			{fill: '#6d8764', stroke: '#3A5431', font: '#ffffff'}, {fill: '#647687', stroke: '#314354', font: '#ffffff'},
			{fill: '#76608a', stroke: '#432D57', font: '#ffffff'}, {fill: '#a0522d', stroke: '#6D1F00', font: '#ffffff'}],
			[{fill: '', stroke: ''}, {fill: mxConstants.NONE, stroke: ''},
			{fill: '#fad7ac', stroke: '#b46504'}, {fill: '#fad9d5', stroke: '#ae4132'},
			{fill: '#b0e3e6', stroke: '#0e8088'}, {fill: '#b1ddf0', stroke: '#10739e'},
			{fill: '#d0cee2', stroke: '#56517e'}, {fill: '#bac8d3', stroke: '#23445d'}],
		    [{fill: '', stroke: ''},
			{fill: '#f5f5f5', stroke: '#666666', gradient: '#b3b3b3'},
			{fill: '#dae8fc', stroke: '#6c8ebf', gradient: '#7ea6e0'},
			{fill: '#d5e8d4', stroke: '#82b366', gradient: '#97d077'},
			{fill: '#ffcd28', stroke: '#d79b00', gradient: '#ffa500'},
			{fill: '#fff2cc', stroke: '#d6b656', gradient: '#ffd966'},
			{fill: '#f8cecc', stroke: '#b85450', gradient: '#ea6b66'},
			{fill: '#e6d0de', stroke: '#996185', gradient: '#d5739d'}],
			[{fill: '', stroke: ''}, {fill: '#eeeeee', stroke: '#36393d'},
			{fill: '#f9f7ed', stroke: '#36393d'}, {fill: '#ffcc99', stroke: '#36393d'},
			{fill: '#cce5ff', stroke: '#36393d'}, {fill: '#ffff88', stroke: '#36393d'},
			{fill: '#cdeb8b', stroke: '#36393d'}, {fill: '#ffcccc', stroke: '#36393d'}]];
		
		/**
		 * Configures custom color schemes.
		 */
		StyleFormatPanel.prototype.customColorSchemes = null;
		
		/**
		 * Adds predefiend styles.
		 */
		var styleFormatPanelInit = StyleFormatPanel.prototype.init;
		
		StyleFormatPanel.prototype.init = function()
		{
			var sstate = this.editorUi.getSelectionState();

			if (this.defaultColorSchemes != null && this.defaultColorSchemes.length > 0 &&
				sstate.style.shape != 'image' && !sstate.containsLabel &&
				sstate.cells.length > 0)
			{
				this.container.appendChild(this.addStyles(this.createPanel()));
			}
			
			styleFormatPanelInit.apply(this, arguments);

			if (sstate.customProperties != null)
			{
				this.container.appendChild(this.addProperties(
					this.createPanel(), sstate.customProperties,
					sstate));
			}
		};

		/**
		 * Overridden to add copy and paste style.
		 */
		var styleFormatPanelAddStyleOps = StyleFormatPanel.prototype.addStyleOps;
		
		StyleFormatPanel.prototype.addStyleOps = function(div)
		{
			var ss = this.editorUi.getSelectionState();

			if (ss.cells.length == 1)
			{
				this.addActions(div, ['copyStyle', 'pasteStyle']);
			}
			else if (ss.cells.length >= 1)
			{
				this.addActions(div, ['pasteStyle', 'pasteData']);
			}

			styleFormatPanelAddStyleOps.apply(this, arguments);
			
			return div;
		};

		/**
		 * Initial collapsed state of the properties panel.
		 */
		EditorUi.prototype.propertiesCollapsed = true;

		/**
		 * Create Properties Panel
		 */
		BaseFormatPanel.prototype.addProperties = function(div, properties, state, hideId)
		{
			var that = this;
			var graph = this.editorUi.editor.graph;
			var secondLevel = [];
			
			function insertAfter(newElem, curElem)
			{
				curElem.parentNode.insertBefore(newElem, curElem.nextSibling);
			};
			
			function applyStyleVal(pName, newVal, prop, delIndex, input)
			{
				if (prop.valueChanged != null)
				{
					prop.valueChanged(newVal, input, state, that);
				}
				else
				{
					graph.getModel().beginUpdate();
					try
					{
						var changedProps = [];
						var changedVals = [];

						if (prop.index != null)
						{
							var allVals = [];
							var curVal = prop.parentRow.nextSibling;
							
							while(curVal && curVal.getAttribute('data-pName') == pName)
							{
								allVals.push(curVal.getAttribute('data-pValue'));
								curVal = curVal.nextSibling;
							}
							
							if (prop.index < allVals.length)
							{
								if (delIndex != null)
								{
									allVals.splice(delIndex, 1);
								}
								else
								{
									allVals[prop.index] = newVal;
								}
							}
							else
							{
								allVals.push(newVal);
							}
							
							if (prop.size != null && allVals.length > prop.size) //trim the array to the specifies size
							{
								allVals = allVals.slice(0, prop.size);
							}
							
							newVal = allVals.join(',');
							
							if (prop.countProperty != null)
							{
								graph.setCellStyles(prop.countProperty, allVals.length, graph.getSelectionCells());
								
								changedProps.push(prop.countProperty);
								changedVals.push(allVals.length);
							}
						}

						graph.setCellStyles(pName, newVal, graph.getSelectionCells());
						changedProps.push(pName);
						changedVals.push(newVal);
						
						if (prop.dependentProps != null)
						{
							for (var i = 0; i < prop.dependentProps.length; i++)
							{
								var defVal = prop.dependentPropsDefVal[i];
								var vals = prop.dependentPropsVals[i];
								
								if (vals.length > newVal)
								{
									vals = vals.slice(0, newVal);
								}
								else
								{
									for (var j = vals.length; j < newVal; j++)
									{
										vals.push(defVal);
									}
								}
								
								vals = vals.join(',');
								graph.setCellStyles(prop.dependentProps[i], vals, graph.getSelectionCells());
								changedProps.push(prop.dependentProps[i]);
								changedVals.push(vals);
							}
						}
						
						if (typeof(prop.onChange) == 'function')
						{
							prop.onChange(graph, newVal);
						}
						
						that.editorUi.fireEvent(new mxEventObject('styleChanged', 'keys', changedProps,
							'values', changedVals, 'cells', graph.getSelectionCells()));
					}
					finally
					{
						graph.getModel().endUpdate();
					}
				}
			}
			
			function setElementPos(td, elem)
			{
				elem.style.left = '0px';
				elem.style.top = '0px';
				elem.style.position = 'relative';
				elem.style.right = '0px';
				elem.style.bottom = '0px';
				elem.style.width = '100%';
				elem.style.maxWidth = '100%';
				elem.style.maxHeight = '100%';
				elem.style.textAlign = 'left';
			};
			
			function createColorBtn(pName, pValue, prop)
			{
				var clrDiv = document.createElement('div');
				clrDiv.style.width = '32px';
				clrDiv.style.height = '8px';
				clrDiv.style.margin = '2px';
				clrDiv.style.borderStyle = 'solid';
				clrDiv.style.borderWidth = '1px';

				function updateBackground(color)
				{
					if (color == null || color == mxConstants.NONE)
					{
						clrDiv.style.background = 'url(\'' + Dialog.prototype.noColorImage + '\')';
					}
					else if (color == 'default')
					{
						// Default is not supported for properties, this is a fallback
						clrDiv.style.background = '';
					}
					else
					{
						var cssColor = mxUtils.getLightDarkColor(color);

						if (mxUtils.isLightDarkColor(color) &&
							cssColor.light != cssColor.dark)
						{
							clrDiv.style.background = 'linear-gradient(to right bottom, ' +
								cssColor.cssText + ' 50%, ' + mxUtils.invertLightDarkColor(cssColor).
								cssText + ' 50.3%)';
						}
						else
						{
							clrDiv.style.background = color;
						}
					}
				};
				
				updateBackground(pValue);

				btn = mxUtils.button('', mxUtils.bind(that, function(evt)
				{
					this.editorUi.pickColor(pValue, function(color)
					{
						applyStyleVal(pName, color, prop);
						updateBackground(color);
					});
					
					mxEvent.consume(evt);
				}));
				
				btn.style.height = '18px';
				btn.style.width = '40px';
				btn.style.marginTop = '1px';
				btn.style.marginLeft = '1px';
				btn.className = 'geColorBtn';
				
				btn.appendChild(clrDiv);
				return btn;
			};
			
			function createDynArrList(pName, pValue, subType, defVal, countProperty, myRow, flipBkg)
			{
				if (pValue != null)
				{
					var vals = pValue.split(',');
					secondLevel.push({name: pName, values: vals, type: subType,
						defVal: defVal, countProperty: countProperty,
						parentRow: myRow, isDeletable: true, flipBkg: flipBkg});
				}
				
				btn = mxUtils.button('+', mxUtils.bind(that, function(evt)
				{
					var beforeElem = myRow;
					var index = 0;
					
					while (beforeElem.nextSibling != null)
					{
						var cur = beforeElem.nextSibling;
						var elemPName = cur.getAttribute('data-pName');

						if (elemPName == pName)
						{
							beforeElem = beforeElem.nextSibling;
							index++;
						}
						else
						{
							break;
						}
					}
					
					var newProp = {type: subType, parentRow: myRow, index: index,
						isDeletable: true, defVal: defVal, countProperty: countProperty};
					var arrItem = createPropertyRow(pName, '', newProp, index % 2 == 0, flipBkg);
					applyStyleVal(pName, defVal, newProp);
					insertAfter(arrItem, beforeElem);
					
					mxEvent.consume(evt);
				}));
				
				btn.style.height = '16px';
				btn.style.width = '25px';
				btn.className = 'geColorBtn';
				
				return btn;
			};
			
			function createStaticArrList(pName, pValue, subType, defVal, size, myRow, flipBkg)
			{
				if (size > 0)
				{
					var vals = new Array(size);
					
					var curVals = pValue != null? pValue.split(',') : [];
					
					for (var i = 0; i < size; i++) 
					{
						vals[i] = curVals[i] != null? curVals[i] : (defVal != null? defVal : '');
					}
					
					secondLevel.push({name: pName, values: vals, type: subType, defVal: defVal, parentRow: myRow, flipBkg: flipBkg, size: size});
				}
				
				return document.createElement('div'); //empty cell
			};
			
			function createCheckbox(pName, pValue, prop)
			{
				var input = document.createElement('input');
				input.type = 'checkbox';
				input.checked = pValue == '1';
				
				mxEvent.addListener(input, 'change', function() 
				{
					applyStyleVal(pName, input.checked? '1' : '0', prop, null, input);
				});
				return input;
			};
			
			function createPropertyRow(pName, pValue, prop, isOdd, flipBkg)
			{
				var pDiplayName = prop.dispName;
				var pType = prop.type;
				var row = document.createElement('tr');
				row.className = 'gePropRow' + (flipBkg? 'Dark' : '') + (isOdd? 'Alt' : '') + ' gePropNonHeaderRow';
				row.setAttribute('data-pName', pName);
				row.setAttribute('data-pValue', pValue);
				var rightAlig = false;
				
				if (prop.index != null)
				{
					row.setAttribute('data-index', prop.index);
					pDiplayName = (pDiplayName != null? pDiplayName : '') + '[' + prop.index + ']';
					rightAlig = true;
				}
				
				var td = document.createElement('td');
				td.className = 'gePropRowCell';
				var label = mxResources.get(pDiplayName, null, pDiplayName);
				mxUtils.write(td, label);
				td.setAttribute('title', label);
				
				if (rightAlig)
				{
					td.style.textAlign = 'right';
				}
					
				row.appendChild(td);
				td = document.createElement('td');
				td.className = 'gePropRowCell';
				td.setAttribute('title', (pValue != null) ?
					decodeURIComponent(pValue) : mxResources.get('none'));

				mxEvent.addListener(td, 'click', mxUtils.bind(that, function(e)
				{
					// Stops current property editor and triggers DOM update
					if (!mxUtils.isAncestorNode(row, document.activeElement) &&
						mxUtils.isAncestorNode(row.parentNode, document.activeElement) &&
						document.activeElement != null && document.activeElement.blur != null)
					{
						document.activeElement.blur();
						mxEvent.consume(e);
					}
				}));
				
				if (pType == 'color')
				{
					td.appendChild(createColorBtn(pName, pValue, prop));
				}
				else if (pType == 'bool' || pType == 'boolean')
				{
					td.appendChild(createCheckbox(pName, pValue, prop));
				}
				else if (pType == 'enum')
				{
					var pEnumList = prop.enumList;

					td.innerHTML = '';
					let valueDiv = document.createElement('div');
					valueDiv.className = 'gePropValue';
					td.appendChild(valueDiv);
					
					for (var i = 0; i < pEnumList.length; i++)
					{
						var op = pEnumList[i];
						
						if (op.val == pValue)
						{
							mxUtils.write(valueDiv, mxResources.get(op.dispName, null, op.dispName));
							break;
						}
					}
					
					mxEvent.addListener(td, 'click', mxUtils.bind(that, function(e)
					{
						// Ignores event if currently editing this property
						var source = mxEvent.getSource(e);

						if ((source != td && source != valueDiv) ||
							mxEvent.isConsumed(e))
						{
							return;
						}

						valueDiv.innerHTML = '';
						var select = document.createElement('select');
						var nullValue = 'null';
						var nullOption = null;
						setElementPos(td, select);

						for (var i = 0; i < pEnumList.length; i++)
						{
							var op = pEnumList[i];
							var opElem = document.createElement('option');
							opElem.value = mxUtils.htmlEntities(op.val);
							mxUtils.write(opElem, mxResources.get(op.dispName, null, op.dispName));
							select.appendChild(opElem);

							if (op.val == null)
							{
								opElem.value = nullValue;
								nullOption = opElem;
							}
						}
						
						select.value = (pValue == null && nullOption != null) ? nullValue : pValue;
						
						valueDiv.appendChild(select);

						mxEvent.addListener(select, 'change', function()
						{
							var newVal = mxUtils.htmlEntities(select.value);

							if (select[select.selectedIndex] == nullOption ||
								newVal.value == nullValue)
							{
								newVal = null;
							}

							applyStyleVal(pName, newVal, prop);
							//set value triggers a redraw of the panel which removes the select and updates the row
						});
			
						select.focus();

						//FF calls blur on focus! so set the event after focusing (not with selects but to be safe)
						mxEvent.addListener(select, 'blur', function()
						{
							valueDiv.innerHTML = '';

							for (let i = 0; i < pEnumList.length; i++)
							{
								let op = pEnumList[i];

								if (op.val == select.value)
								{
									mxUtils.write(valueDiv, mxResources.get(op.dispName, null, op.dispName));
									break;
								}
							}
						});
					}));
				}
				else if (pType == 'dynamicArr')
				{
					td.appendChild(createDynArrList(pName, pValue, prop.subType, prop.subDefVal, prop.countProperty, row, flipBkg));
				}
				else if (pType == 'staticArr')
				{
					td.appendChild(createStaticArrList(pName, pValue, prop.subType, prop.subDefVal, prop.size, row, flipBkg));
				}
				else if (pType == 'readOnly')
				{
					var inp = document.createElement('input');
					inp.setAttribute('readonly', '');
					inp.value = pValue;
					inp.style.borderWidth = '0px';

					if (pName == 'id' || pName == 'shape')
					{
						row.firstChild.innerHTML = '';
						row.firstChild.setAttribute('colspan', '2');

						inp.style.flexGrow = '1';
						inp.style.marginLeft = '4px';
						inp.style.textAlign = pName == 'id' ?
							'center' : 'left';
						inp.setAttribute('title', pValue);

						var div = document.createElement('div');
						mxUtils.write(div, mxResources.get(pName) + ':');
						div.style.display = 'inline-flex';
						div.style.alignItems = 'center';
						div.style.width = '100%';

						div.appendChild(inp);
						row.firstChild.appendChild(div);
					}
					else
					{
						inp.style.width = '96px';
						td.appendChild(inp);
					}
				}
				else
				{
					td.innerHTML = '';
					let valueDiv = document.createElement('div');
					valueDiv.className = 'gePropValue';
					td.appendChild(valueDiv);
					valueDiv.innerHTML = mxUtils.htmlEntities(decodeURIComponent(pValue));

					mxEvent.addListener(td, 'click', mxUtils.bind(that, function(e)
					{
						// Ignores event if currently editing this property
						var source = mxEvent.getSource(e);

						if ((source != td && source != valueDiv) ||
							mxEvent.isConsumed(e))
						{
							return;
						}

						valueDiv.innerHTML = '';
						var input = document.createElement('input');
						setElementPos(valueDiv, input);
						input.value = decodeURIComponent(pValue);
						input.className = 'gePropEditor';
						
						if ((pType == 'int' || pType == 'float') && !prop.allowAuto)
						{
							input.type = 'number';
							input.step = pType == 'int'? '1' : 'any';
							
							if (prop.min != null)
							{
								input.min = parseFloat(prop.min); 
							}
							
							if (prop.max != null)
							{
								input.max = parseFloat(prop.max);
							}
						}
						
						valueDiv.appendChild(input);

						function setInputVal()
						{
							var inputVal = input.value;
							inputVal = inputVal.length == 0 && pType != 'string' &&
								pType != 'numbers'? 0 : inputVal;
							
							if (prop.allowAuto)
							{
								if (inputVal.trim != null && inputVal.trim().
									toLowerCase() == 'auto')
								{
									inputVal = 'auto';
									pType = 'string';
								}
								else
								{
									inputVal = parseFloat(inputVal);
									inputVal = isNaN(inputVal)? 0 : inputVal;
								}
							}
							
							if (prop.min != null && inputVal < prop.min)
							{
								inputVal = prop.min;
							} 
							else if (prop.max != null && inputVal > prop.max)
							{
								inputVal = prop.max;
							}

							var newVal = null;

							try
							{
								newVal = (pType == 'numbers') ? inputVal.match(/\d+/g).map(Number).join(' ') :
									encodeURIComponent((pType == 'int'? parseInt(inputVal) : inputVal) + '');
							}
							catch(e)
							{
								// ignores parsing errors
							}
							
							applyStyleVal(pName, newVal, prop, null, input);
						}
						
						mxEvent.addListener(input, 'keypress', function(e)
						{
							if (e.keyCode == 13) 
							{
								setInputVal();
								//set value triggers a redraw of the panel which removes the input
							}
						});
						
						input.focus();
						
						//FF calls blur on focus! so set the event after focusing
						mxEvent.addListener(input, 'blur', function() 
						{
							setInputVal();
						});
					}));
				}

				if (prop.isDeletable)
				{
					var delBtn = mxUtils.button('-', mxUtils.bind(that, function(evt)
					{
						//delete the node by refreshing the properties
						applyStyleVal(pName, '', prop, prop.index);
						
						mxEvent.consume(evt);
					}));
					
					delBtn.style.height = '16px';
					delBtn.style.width = '25px';
					delBtn.style.float = 'right';
					delBtn.className = 'geColorBtn';
					td.appendChild(delBtn);
				}
				
				row.appendChild(td);
				return row;
			};
			
			var grid = document.createElement('table');
			grid.className = 'geProperties geFullWidthElement';
			//create header row
			var hrow = document.createElement('tr');
			hrow.className = 'gePropHeader';
			var th = document.createElement('th');
			th.className = 'gePropHeaderCell';
			th.style.paddingLeft = '16px';
			th.style.backgroundRepeat = 'no-repeat';
			th.style.backgroundPosition = '-2px 50%';
			th.style.backgroundSize = '20px';
			mxUtils.write(th, mxResources.get('property'));
			hrow.style.cursor = 'pointer';
			
			var onFold = function()
			{
				var rows = grid.querySelectorAll('.gePropNonHeaderRow');
				var display;
				
				if (!that.editorUi.propertiesCollapsed)
				{
					th.style.backgroundImage = 'url(\'' + Editor.arrowDownImage + '\')';
					display = '';
				}
				else
				{
					th.style.backgroundImage = 'url(\'' + Editor.arrowRightImage + '\')';
					display = 'none';
					
					for (var e = div.childNodes.length - 1; e >= 0 ; e--)
					{
						//Blur can be executed concurrently with this method and the element is removed before removing it here
						try
						{
							var child = div.childNodes[e]; 
							var nodeName = child.nodeName.toUpperCase();
							
							if (nodeName == 'INPUT' || nodeName == 'SELECT')
							{
								div.removeChild(child);
							}
						}
						catch(ex){}
					}
				}
				
				for (var r = 0; r < rows.length; r++)
				{
					rows[r].style.display = display;
				}
			};

			mxEvent.addListener(hrow, 'click', function()
			{
				that.editorUi.propertiesCollapsed = !that.editorUi.propertiesCollapsed;
				onFold();
			});

			hrow.appendChild(th);
			var th2 = document.createElement('th');
			th2.className = 'gePropHeaderCell';
			th2.style.paddingLeft = '4px';
			mxUtils.write(th2, mxResources.get('value'));
			hrow.appendChild(th2);
			grid.appendChild(hrow);
			
			var isOdd = false;
			var flipBkg = false;
			
			var cellId = null;
			
			if (state.vertices.length == 1 && state.edges.length == 0)
			{
				cellId = state.vertices[0].id;
			}
			else if (state.vertices.length == 0 && state.edges.length == 1)
			{
				cellId = state.edges[0].id;
			}
			
			//Add it to top (always)
			if (cellId != null && !hideId)
			{
				grid.appendChild(createPropertyRow('id', mxUtils.htmlEntities(cellId),
					{dispName: 'id', type: 'readOnly'}, true, false));
				
				if (state.style != null && state.style.shape != null)
				{
					grid.appendChild(createPropertyRow('shape', mxUtils.htmlEntities(state.style.shape),
						{dispName: 'shape', type: 'readOnly'}, true, false));
				}
			}
			
			for (var key in properties)
			{
				var prop = properties[key];
				
				if (typeof(prop.isVisible) == 'function')
				{
					if (!prop.isVisible(state, this)) continue;
				}
				
				if (!prop.primary)
				{
					var pValue = (prop.getValue != null) ? prop.getValue(state, this) : (state.style[key] != null? mxUtils.htmlEntities(state.style[key] + '') :
						((prop.getDefaultValue != null) ? prop.getDefaultValue(state, this) : prop.defVal)); //or undefined if defVal is undefined

					if (prop.type == 'separator')
					{
						flipBkg = !flipBkg;
						continue;
					}
					else if (prop.type == 'staticArr') //if dynamic values are needed, a more elegant technique is needed to replace such values
					{
						prop.size = parseInt(state.style[prop.sizeProperty] || properties[prop.sizeProperty].defVal) || 0;
					}
					else if (prop.dependentProps != null)
					{
						var dependentProps = prop.dependentProps;
						var dependentPropsVals = [];
						var dependentPropsDefVal = [];
						
						for (var i = 0; i < dependentProps.length; i++)
						{
							if (properties[dependentProps[i]] != null)
							{
								var propVal = state.style[dependentProps[i]];
								dependentPropsDefVal.push(properties[dependentProps[i]].subDefVal);
								dependentPropsVals.push(propVal != null? propVal.split(',') : []);
							}
						}
						
						prop.dependentPropsDefVal = dependentPropsDefVal;
						prop.dependentPropsVals = dependentPropsVals;
					}
					
					grid.appendChild(createPropertyRow(key, pValue, prop, isOdd, flipBkg));
					
					isOdd = !isOdd;
				}
			}
			
			for (var i = 0; i < secondLevel.length; i++)
			{
				var prop = secondLevel[i];
				var insertElem = prop.parentRow;
					
				for (var j = 0; j < prop.values.length; j++)
				{
					//mxUtils.clone failed because of the HTM element, so manual cloning is used
					var iProp = {type: prop.type, parentRow: prop.parentRow, isDeletable: prop.isDeletable, index: j,
							defVal: prop.defVal, countProperty: prop.countProperty, size: prop.size};
					var arrItem = createPropertyRow(prop.name, prop.values[j], iProp, j % 2 == 0, prop.flipBkg);
					insertAfter(arrItem, insertElem);
					insertElem = arrItem;
				}
			}
			
			div.appendChild(grid);
			onFold();
			
			return div;
		};
		
		/**
		 * Creates the buttons for the predefined styles.
		 */
		StyleFormatPanel.prototype.addStyles = function(div)
		{
			if (this.defaultColorSchemes != null)
			{
				var ui = this.editorUi;
				var graph = ui.editor.graph;
				var picker = document.createElement('div');
				picker.style.padding = '6px 20px 0 24px';
				div.appendChild(picker);

				// Maximum palettes to switch the switcher
				var maxEntries = 10;
							
				// Selector
				var switcher = document.createElement('div');
				switcher.className = 'geSwitcher';
				switcher.style.padding = '4px 18px 6px 0px';
				
				var dots = [];
				
				for (var i = 0; i < this.defaultColorSchemes.length; i++)
				{
					var dot = document.createElement('div');
					dot.className = 'geSwitcherDot';
					
					(mxUtils.bind(this, function(index)
					{
						mxEvent.addListener(dot, 'click', mxUtils.bind(this, function()
						{
							setScheme(index);
						}));
					}))(i);
					
					dots.push(dot);
					switcher.appendChild(dot);
				}
				
				var setScheme = mxUtils.bind(this, function(index)
				{
					if (dots[index] != null)
					{
						if (this.format.currentScheme != null && dots[this.format.currentScheme] != null)
						{
							dots[this.format.currentScheme].style.background = 'transparent';
						}
						
						this.format.currentScheme = index;
						updateScheme(this.defaultColorSchemes[this.format.currentScheme]);
						dots[this.format.currentScheme].style.background = '#84d7ff';
					}
				});
				
				var updateScheme = mxUtils.bind(this, function(colorsets)
				{
					var defaults = mxUtils.clone(graph.defaultVertexStyle);
					defaults[mxConstants.STYLE_STROKECOLOR] = (defaults[mxConstants.STYLE_STROKECOLOR] != null) ?
						defaults[mxConstants.STYLE_STROKECOLOR] : 'default';
					defaults[mxConstants.STYLE_FILLCOLOR] = (defaults[mxConstants.STYLE_FILLCOLOR] != null) ?
						defaults[mxConstants.STYLE_FILLCOLOR] : 'default';
					defaults[mxConstants.STYLE_FONTCOLOR] = (defaults[mxConstants.STYLE_FONTCOLOR] != null) ?
						defaults[mxConstants.STYLE_FONTCOLOR] : 'default';
					graph.replaceDefaultColors(new mxCell(), defaults);

					var addButton = mxUtils.bind(this, function(colorset)
					{
						var btn = mxUtils.button('', mxUtils.bind(this, function(evt)
						{
							graph.getModel().beginUpdate();
							try
							{
								var cells = ui.getSelectionState().cells;
								
								for (var i = 0; i < cells.length; i++)
								{
									var style = graph.getModel().getStyle(cells[i]);
									
									if (colorset != null)
									{
										if (!mxEvent.isShiftDown(evt))
										{
											if (colorset['fill'] == '' || colorset['fill'] == null)
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_FILLCOLOR, null);
											}
											else
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_FILLCOLOR, colorset['fill']);
											}

											if (colorset['gradient'] == '' || colorset['gradient'] == null)
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_GRADIENTCOLOR, null);
											}
											else
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_GRADIENTCOLOR, colorset['gradient']);
											}

											if (!mxEvent.isControlDown(evt) && (!mxClient.IS_MAC || !mxEvent.isMetaDown(evt)) &&
												graph.getModel().isVertex(cells[i]))
											{
												if (colorset['font'] == '' || colorset['font'] == null)
												{
													style = mxUtils.setStyle(style, mxConstants.STYLE_FONTCOLOR, null);
												}
												else
												{
													style = mxUtils.setStyle(style, mxConstants.STYLE_FONTCOLOR, colorset['font']);
												}
											}
										}
										
										if (!mxEvent.isAltDown(evt))
										{
											if (colorset['stroke'] == '' || colorset['fill'] == null)
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_STROKECOLOR, null);
											}
											else
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_STROKECOLOR, colorset['stroke']);
											}
										}
									}
									else
									{
										style = mxUtils.setStyle(style, mxConstants.STYLE_FILLCOLOR, null);
										style = mxUtils.setStyle(style, mxConstants.STYLE_STROKECOLOR, null);
										style = mxUtils.setStyle(style, mxConstants.STYLE_GRADIENTCOLOR, null);
										
										if (graph.getModel().isVertex(cells[i]))
										{
											style = mxUtils.setStyle(style, mxConstants.STYLE_FONTCOLOR, null);
										}
									}

									graph.getModel().setStyle(cells[i], style);
								}
							}
							finally
							{
								graph.getModel().endUpdate();
							}
						}));
		
						btn.className = 'geStyleButton';
						btn.style.width = '36px';
						btn.style.height = (this.defaultColorSchemes.length <= maxEntries) ? '24px' : '30px';
						btn.style.margin = '0px 6px 6px 0px';
						
						if (colorset != null)
						{
							var b = '1px solid';
							
							if (colorset['border'] != null)
							{
								b = colorset['border'];
							}
							
							if (colorset['gradient'] != null)
							{
								btn.style.backgroundImage = 'linear-gradient(' +
									mxUtils.getLightDarkColor(colorset['fill']).cssText + ' 0px,' +
									mxUtils.getLightDarkColor(colorset['gradient']).cssText + ' 100%)';
							}
							else if (colorset['fill'] == mxConstants.NONE)
							{
								btn.style.background = 'url(\'' + Dialog.prototype.noColorImage + '\')';
							}
							else if (colorset['fill'] == null || colorset['fill'] == '')
							{
								btn.style.backgroundColor = mxUtils.getLightDarkColor(
									mxUtils.getValue(defaults, mxConstants.STYLE_FILLCOLOR,
										'#ffffff')).cssText;
							}
							else
							{
								var cssColor = mxUtils.getLightDarkColor(colorset['fill']);
								btn.style.backgroundImage = 'linear-gradient(to right bottom, ' +
									cssColor.cssText + ' 50%, ' + cssColor.light + ' 50.3%)';
							}
							
							if (colorset['stroke'] == null || colorset['stroke'] == mxConstants.NONE)
							{
								btn.style.border = b + ' transparent';
							}
							else if (colorset['stroke'] == '')
							{
								btn.style.border = '1px solid ' + mxUtils.getLightDarkColor(
									mxUtils.getValue(defaults, mxConstants.STYLE_STROKECOLOR,
										'#000000')).cssText;
							}
							else
							{
								var cssColor = mxUtils.getLightDarkColor(colorset['stroke']);
								btn.style.border = b + ' ' + cssColor.cssText;
								btn.style.borderRightColor = cssColor.light;
								btn.style.borderBottomColor = btn.style.borderRightColor;
							}

							if (colorset['title'] != null)
							{
								btn.setAttribute('title', colorset['title']);
							}
						}
						else
						{
							var bg = mxUtils.getValue(defaults, mxConstants.STYLE_FILLCOLOR, '#ffffff');
							var bd = mxUtils.getValue(defaults, mxConstants.STYLE_STROKECOLOR, '#000000');
							
							btn.style.backgroundColor = bg;
							btn.style.border = '1px solid ' + bd;
						}

						btn.style.borderRadius = '0';
						picker.appendChild(btn);

						if (colorset != null && colorset['gradient'] != null)
						{
							var lightBtn = btn.cloneNode(false);
							lightBtn.style.backgroundImage = 'linear-gradient(light-dark(transparent, ' +
								mxUtils.getLightDarkColor(colorset['fill']).light + ') 0px, ' +
									'light-dark(transparent, ' + mxUtils.getLightDarkColor(
										colorset['gradient']).light + ') 100%)';
							lightBtn.style.clipPath = 'polygon(0 100%, 100% 0, 100% 100%)';
							lightBtn.style.backgroundColor = 'transparent';
							picker.appendChild(lightBtn);
							lightBtn.style.marginLeft = '-42px';

							mxEvent.addListener(lightBtn, 'click', function()
							{
								btn.click();
							});
						}
					});
					
					picker.innerText = '';
					
					if (colorsets != null)
					{
						for (var i = 0; i < colorsets.length; i++)
						{
							if (i > 0 && mxUtils.mod(i, 4) == 0)
							{
								mxUtils.br(picker);
							}
							
							addButton(colorsets[i]);
						}
					}
				});

				if (this.format.currentScheme == null)
				{
					setScheme(Math.min(dots.length - 1, Editor.isDarkMode()
						? 1 : (urlParams['sketch'] == '1' ? 5 : 0)));
				}
				else
				{
					setScheme(this.format.currentScheme);
				}
				
				var bottom = (this.defaultColorSchemes.length <= maxEntries) ? 43 : 23;

				var left = document.createElement('div');
				left.className = 'geButton';
				left.style.cssText = 'position:absolute;left:0px;bottom:' + bottom + 'px;width:20px;' +
					'background-image:url(' + Editor.chevronLeftImage + ');';
				
				mxEvent.addListener(left, 'click', mxUtils.bind(this, function()
				{
					setScheme(mxUtils.mod(this.format.currentScheme - 1, this.defaultColorSchemes.length));
				}));
				
				var right = document.createElement('div');
				right.className = 'geButton';
				right.style.cssText = 'position:absolute;left:186px;bottom:' + bottom + 'px;width:20px;' +
					'background-image:url(' + Editor.chevronRightImage + ');';

				if (this.defaultColorSchemes.length > 1)
				{
					div.appendChild(left);
					div.appendChild(right);
				}
				
				mxEvent.addListener(right, 'click', mxUtils.bind(this, function()
				{
					setScheme(mxUtils.mod(this.format.currentScheme + 1, this.defaultColorSchemes.length));
				}));
				
				updateScheme(this.defaultColorSchemes[this.format.currentScheme]);
				
				if (this.defaultColorSchemes.length <= maxEntries)
				{
					div.appendChild(switcher);
				}
			}
			
			return div;
		};
	}
	
	/**
	 * Maps fonts to font-face CSS.
	 */
	Graph.fontMapping = {'https://fonts.googleapis.com/css?family=Architects+Daughter':
		'@font-face { font-family: "Architects Daughter"; ' + 
		'src: url(' + STYLE_PATH + '/fonts/ArchitectsDaughter-Regular.ttf) format("truetype"); }'};

	/**
	 * Lookup table for mapping from font URL and name to elements in the DOM.
	 */
	Graph.customFontElements = {};

	/**
	 * Returns true if the given font URL references a Google font.
	 */
	Graph.isGoogleFontUrl = function(url)
	{
		return url.substring(0, Editor.GOOGLE_FONTS.length) == Editor.GOOGLE_FONTS ||
			url.substring(0, Editor.GOOGLE_FONTS_CSS2.length) == Editor.GOOGLE_FONTS_CSS2;
	};

	/**
	 * Returns true if the given font URL is a CSS file.
	 */
	Graph.isCssFontUrl = function(url)
	{
		return Graph.isGoogleFontUrl(url);
	};

	/**
	 * Uses CSS2 for Google fonts to support bold font style eg.
	 * https://fonts.googleapis.com/css?family=IBM+Plex+Sans is rewritten as
	 * https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500
	 */
	Graph.rewriteGoogleFontUrl = function(url)
	{
		if (url != null && url.substring(0, Editor.GOOGLE_FONTS.length) == Editor.GOOGLE_FONTS)
		{
			url = Editor.GOOGLE_FONTS_CSS2 + url.substring(Editor.GOOGLE_FONTS.length) + ':wght@400;500';
		}

		return url;
	};

	/**
	 * Creates the DOM node for the custom font.
	 */
	Graph.createFontElement = function(name, url)
	{
		var elt = null;
		var style = Graph.fontMapping[url];

		if (style == null && Graph.isCssFontUrl(url))
		{
			elt = document.createElement('link');
			elt.setAttribute('rel', 'stylesheet');
			elt.setAttribute('type', 'text/css');
			elt.setAttribute('charset', 'UTF-8');
			elt.setAttribute('href', Graph.rewriteGoogleFontUrl(url));
		}
		else
		{
			if (style == null)
			{
				style = '@font-face {\n' +	
					'font-family: "' + name + '";\n' + 	
					'src: url("' + url + '");\n}'
			}

			elt = document.createElement('style');
			mxUtils.write(elt, style);
		}
		
		return elt;
	};
		
	/**
	 * Adds an entry to the recent custom fonts list.
	 */
	Graph.addRecentCustomFont = function(key, entry)
	{
		// Hook for registering recent custom fonts in the UI
	};

	/**
	 * Adds a font to the document.
	 */
	Graph.addFont = function(name, url, callback, elementLookup)
	{
		if (name != null && name.length > 0 && url != null && url.length > 0)
		{
			elementLookup = (elementLookup != null) ?
				elementLookup : Graph.customFontElements;
			var key = name.toLowerCase();
			
			// Blocks UI fonts from being overwritten
			if (key != 'helvetica' && name != 'arial' && key != 'sans-serif')
			{
				var entry = elementLookup[key];
				
				// Replaces element if URL has changed
				if (entry != null && entry.url != url)
				{
					entry.elt.parentNode.removeChild(entry.elt);
					entry = null;
				}
				
				if (entry == null)
				{
					var realUrl = url;
					
					// Fixes possible mixed content by using proxy
					if (url.substring(0, 5) == 'http:')
					{
						realUrl = PROXY_URL + '?url=' + encodeURIComponent(url);
					}

					entry = {name: name, url: url, elt: Graph.createFontElement(name, realUrl)};
					elementLookup[key] = entry;
					var head = (elementLookup == Graph.customFontElements) ?
						document.getElementsByTagName('head')[0] : null;
					
					if (callback != null)
					{
						if (entry.elt.nodeName.toLowerCase() == 'link')
						{
							entry.elt.onload = callback;
							entry.elt.onerror = callback;
						}
						else
						{
							callback();
						}
					}
						
					if (head != null)
					{
						head.appendChild(entry.elt);
					}
				}
				else if (callback != null)
				{
					callback();
				}
			}
			else if (callback != null)
			{
				callback();
			}
		}
		else if (callback != null)
		{
			callback();
		}
				
		return name;
	};

	/**
	 * Returns the URL for the given font name if it exists in the document.
	 * Otherwise it returns the given URL.
	 */
	Graph.getFontUrl = function(name, url)
	{
		var font = Graph.customFontElements[name.toLowerCase()];
		
		if (font != null)
		{
			url = font.url;
		}
		
		return url;
	};
	
	/**
	 * Processes the fonts in the given element and its descendants.
	 */
	Graph.processFontAttributes = function(elt)
	{
		var elts = elt.getElementsByTagName('*');
		
		for (var i = 0; i < elts.length; i++)
		{
			var url = elts[i].getAttribute('data-font-src');
			
			if (url != null)
			{
				var name = (elts[i].nodeName == 'FONT') ?
					elts[i].getAttribute('face') :
					elts[i].style.fontFamily;
	
				if (name != null)
				{
					Graph.addFont(name, url);
				}
			}
		}		
	};
	
	/**
	 * Processes the font in the given cell style.
	 */
	Graph.processFontStyle = function(style)
	{
		if (style != null)
		{
			var url = mxUtils.getValue(style, 'fontSource', null);
	
			if (url != null)
			{
				var name = mxUtils.getValue(style, mxConstants.STYLE_FONTFAMILY, null);
				
				if (name != null)
				{
					try
					{
						Graph.addFont(name, decodeURIComponent(url));
					}
					catch (e)
					{
						// ignore
					}
				}
			}
		}
		
		return style;
	};

	/**
	 * Changes the default stylename so that it matches the old named style
	 * if one was specified in the XML.
	 */
	Graph.prototype.defaultThemeName = 'default-style2';
	
	/**
	 * Contains the last XML that was pasted.
	 */
	Graph.prototype.lastPasteXml = null;
	
	/**
	 * Contains the number of times the last XML was pasted.
	 */
	Graph.prototype.pasteCounter = 0;
	
	/**
	 * Graph Overrides
	 */
	Graph.prototype.defaultScrollbars = urlParams['sb'] != '0';

	/**
	 * Specifies if the page should be visible for new files. Default is true.
	 */
	Graph.prototype.defaultPageVisible = urlParams['pv'] != '0';

	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowColor = '#3D4574';
	
	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowOpacity = '0.4';

	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowBlur = '1.7';
	
	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowSize = '3';

	/**
	 * Enables move of bends/segments without selecting.
	 */
	Graph.prototype.hiddenTags = [];
	
	/**
	 * Enables move of bends/segments without selecting.
	 */
	Graph.prototype.defaultMathEnabled = false;
	
	/**
	 * Adds rack child layout style.
	 */
	var graphInit = Graph.prototype.init;
	
	Graph.prototype.init = function()
	{
		graphInit.apply(this, arguments);

		// Array of hidden tags used in isCellVisible override
		this.hiddenTags = [];

		//TODO initialize Freehand in the correct location!
		if (window.mxFreehand)
		{
			this.freehand = new mxFreehand(this);
		}
		
		// Override insert location for current mouse point
		var mouseEvent = null;
		
		function setMouseEvent(evt)
		{
			mouseEvent = evt;
		};
		
		mxEvent.addListener(this.container, 'mouseenter', setMouseEvent);
		mxEvent.addListener(this.container, 'mousemove', setMouseEvent);
		mxEvent.addListener(this.container, 'mouseleave', function(evt)
		{
			mouseEvent = null;
		});
				
		// Extends getInsertPoint to use the current mouse location
		this.isMouseInsertPoint = function()
		{
			return mouseEvent != null;
		};
		
		var getInsertPoint = this.getInsertPoint;
		
		this.getInsertPoint = function(ignoreMouseEvent)
		{
			if (mouseEvent != null && !ignoreMouseEvent)
			{
				return this.getPointForEvent(mouseEvent);
			}
			
			return getInsertPoint.apply(this, arguments);
		};
		
		var layoutManagerGetLayout = this.layoutManager.getLayout;
		
		this.layoutManager.getLayout = function(cell)
		{
			// Workaround for possible invalid style after change and before view validation
			var style = this.graph.getCellStyle(cell);
			
			// mxRackContainer may be undefined as it is dynamically loaded at render time
			if (style != null)
			{
				if (style['childLayout'] == 'rack')
				{
					var rackLayout = new mxStackLayout(this.graph, false);
					var unitSize = 20;
					
					if (style['rackUnitSize'] != null)
					{
						rackLayout.gridSize = parseFloat(style['rackUnitSize']);
					}
					else
					{
						rackLayout.gridSize = (typeof mxRackContainer !== 'undefined') ? mxRackContainer.unitSize : unitSize;
					}
					
					rackLayout.marginLeft = style['marginLeft'] || 0;
					rackLayout.marginRight = style['marginRight'] || 0;
					rackLayout.marginTop = style['marginTop'] || 0;
					rackLayout.marginBottom = style['marginBottom'] || 0;
					rackLayout.allowGaps = style['allowGaps'] || 0;
					rackLayout.horizontal = mxUtils.getValue(style, 'horizontalRack', '0') == '1';
					rackLayout.resizeParent = false;
					rackLayout.fill = true;
					
					return rackLayout;
				}
			}
			
			return layoutManagerGetLayout.apply(this, arguments);
		}
		
		this.updateGlobalUrlVariables();
	};

	/**
	 * Adds support for custom fonts in cell styles.
	 */
	var graphPostProcessCellStyle = Graph.prototype.postProcessCellStyle;
	Graph.prototype.postProcessCellStyle = function(cell, style)
	{
		return Graph.processFontStyle(graphPostProcessCellStyle.apply(this, arguments));
	};

	/**
	 * Handles custom fonts in labels.
	 */
	var mxSvgCanvas2DUpdateTextNodes = mxSvgCanvas2D.prototype.updateTextNodes;
	mxSvgCanvas2D.prototype.updateTextNodes = function(x, y, w, h, align, valign, wrap, overflow, clip, rotation, dir, g)
	{
		mxSvgCanvas2DUpdateTextNodes.apply(this, arguments);
		Graph.processFontAttributes(g);
	};
	
	/**
	 * Handles custom fonts in labels.
	 */
	var mxTextRedraw = mxText.prototype.redraw;
	mxText.prototype.redraw = function()
	{
		mxTextRedraw.apply(this, arguments);
		
		// Handles label rendered without foreign object
		if (this.node != null && this.node.nodeName == 'DIV')
		{
			Graph.processFontAttributes(this.node);
		}
	};

	/**
	 * Creates the tags dialog.
	 */
	Graph.prototype.createTagsDialog = function(isEnabled, invert, addFn, helpButton)
	{
		var graph = this;
		var allTags = graph.hiddenTags.slice();
		
		var div = document.createElement('div');
		div.style.userSelect = 'none';
		div.style.overflow = 'hidden';
		div.style.padding = '10px';
		div.style.height = '100%';
		
		var tagCloud = document.createElement('div');
		tagCloud.style.position = 'absolute';
		tagCloud.style.boxSizing = 'border-box';
		tagCloud.style.userSelect = 'none';
		tagCloud.style.overflow = 'auto';
		tagCloud.style.padding = '3px';
		tagCloud.style.left = '0px';
		tagCloud.style.right = '0px';
		tagCloud.style.top = '0px';
		tagCloud.style.bottom = '0px';
		div.appendChild(tagCloud);

		function removeInvisibleSelectionCells()
		{
			var cells = graph.getSelectionCells();
			var visible = [];

			for (var i = 0; i < cells.length; i++)
			{
				if (graph.isCellVisible(cells[i]))
				{
					visible.push(cells[i]);	
				}
			}

			graph.setSelectionCells(visible);
		};

		function setAllVisible(visible)
		{
			graph.setHiddenTags(visible ? [] : allTags.slice());
			removeInvisibleSelectionCells();
			graph.refresh();
		};
		
		graph.addListener(mxEvent.ROOT, function()
		{
			allTags = graph.hiddenTags.slice();
		});
	
		function refreshTags(tags, selected)
		{
			tagCloud.innerText = '';
	
			if (tags.length > 0)
			{
				var table = document.createElement('table');
				table.setAttribute('cellpadding', '2');
				table.style.boxSizing = 'border-box';
				table.style.tableLayout = 'fixed';
				table.style.width = '100%';
	
				var tbody = document.createElement('tbody');

				if (tags != null && tags.length > 0)
				{
					for (var i = 0; i < tags.length; i++)
					{
						(function(tag)
						{
							function setTagVisible()
							{
								var temp = allTags.slice();
								var index = mxUtils.indexOf(temp, tag);
								temp.splice(index, 1);
								graph.setHiddenTags(temp);
								removeInvisibleSelectionCells();
								graph.refresh();
							};

							function selectCells()
							{
								var cells = graph.getCellsForTags(
									[tag], null, null, true);

								if (graph.isEnabled())
								{
									graph.setSelectionCells(cells);
								}
								else
								{
									graph.highlightCells(cells, null, null, 70);
								}
							};

							var visible = mxUtils.indexOf(graph.hiddenTags, tag) < 0;
							var row = document.createElement('tr');
							var td = document.createElement('td');
							td.style.align = 'center';
							td.style.width = '16px';

							var img = document.createElement('img');
							img.setAttribute('src', visible ? Editor.visibleImage : Editor.hiddenImage);
							img.setAttribute('title', mxResources.get(visible ? 'hideIt' : 'show', [tag]));
							mxUtils.setOpacity(img, visible ? 75 : 25);
							img.className = 'geAdaptiveAsset';
							img.style.verticalAlign = 'middle';
							img.style.cursor = 'pointer';
							img.style.width = '16px';
							
							if (invert || Editor.isDarkMode())
							{
								img.style.filter = 'invert(100%)';
							}
							
							td.appendChild(img);

							mxEvent.addListener(img, 'click', function(evt)
							{
								if (mxEvent.isShiftDown(evt))
								{
									setAllVisible(mxUtils.indexOf(graph.hiddenTags, tag) >= 0);
								}
								else
								{
									graph.toggleHiddenTag(tag);
									removeInvisibleSelectionCells();
									graph.refresh();
								}

								mxEvent.consume(evt);
							});
							
							row.appendChild(td);

							td = document.createElement('td');
							td.style.align = 'center';
							td.style.width = '16px';

							var img = document.createElement('img');
							img.setAttribute('src', Editor.selectImage);
							img.setAttribute('title', mxResources.get('select'));
							mxUtils.setOpacity(img, visible ? 75 : 25);
							img.className = 'geAdaptiveAsset';
							img.style.verticalAlign = 'middle';
							img.style.cursor = 'pointer';
							img.style.width = '16px';
							
							if (invert || Editor.isDarkMode())
							{
								img.style.filter = 'invert(100%)';
							}

							mxEvent.addListener(img, 'click', function(evt)
							{
								setAllVisible(true);
								selectCells();	
								mxEvent.consume(evt);
							});
							
							td.appendChild(img);
							row.appendChild(td);

							td = document.createElement('td');
							td.style.overflow = 'hidden';
							td.style.whiteSpace = 'nowrap';
							td.style.textOverflow = 'ellipsis';
							td.style.verticalAlign = 'middle';
							td.style.cursor = 'pointer';
							td.setAttribute('title', tag);
	
							a = document.createElement('a');
							mxUtils.write(a, tag);
							a.style.textOverflow = 'ellipsis';
							a.style.position = 'relative';
							mxUtils.setOpacity(a, visible ? 100 : 40);
							td.appendChild(a);
	
							mxEvent.addListener(td, 'click', (function(evt)
							{
								if (mxEvent.isShiftDown(evt))
								{
									setAllVisible(true);
									selectCells();	
								}
								else
								{
									if (visible && graph.hiddenTags.length > 0)
									{
										setAllVisible(true);
									}
									else
									{
										setTagVisible();
									}
								}

								mxEvent.consume(evt);
							}));

							row.appendChild(td);

							if (graph.isEnabled())
							{
								td = document.createElement('td');
								td.style.verticalAlign = 'middle';
								td.style.textAlign = 'center';
								td.style.width = '18px';
	
								if (selected == null)
								{
									td.style.align = 'center';
									td.style.width = '16px';
		
									var img = document.createElement('img');
									img.setAttribute('src', Editor.trashImage);
									img.setAttribute('title', mxResources.get('removeIt', [tag]));
									mxUtils.setOpacity(img, visible ? 75 : 25);
									img.className = 'geAdaptiveAsset';
									img.style.verticalAlign = 'middle';
									img.style.cursor = 'pointer';
									img.style.width = '16px';

									if (invert || Editor.isDarkMode())
									{
										img.style.filter = 'invert(100%)';
									}

									mxEvent.addListener(img, 'click', function(evt)
									{
										var idx = mxUtils.indexOf(allTags, tag);

										if (idx >= 0)
										{
											allTags.splice(idx, 1);
										}

										graph.removeTagsForCells(
											graph.model.getDescendants(
											graph.model.getRoot()), [tag]);
										graph.refresh();

										mxEvent.consume(evt);
									});

									td.appendChild(img);
								}
								else
								{
									var cb2 = document.createElement('input');
									cb2.setAttribute('type', 'checkbox');
									cb2.style.margin = '0px';
		
									cb2.defaultChecked = (selected != null &&
										mxUtils.indexOf(selected, tag) >= 0);
									cb2.checked = cb2.defaultChecked;
									cb2.style.background = 'transparent';
									cb2.setAttribute('title', mxResources.get(
										cb2.defaultChecked ?
										'removeIt' : 'add', [tag]));
		
									mxEvent.addListener(cb2, 'change', function(evt)
									{
										if (cb2.checked)
										{
											graph.addTagsForCells(graph.getSelectionCells(), [tag]);
										}
										else
										{
											graph.removeTagsForCells(graph.getSelectionCells(), [tag]);
										}
									
										mxEvent.consume(evt);
									});
		
									td.appendChild(cb2);
								}

								row.appendChild(td);
							}
	
							tbody.appendChild(row);
						})(tags[i]);
					}
				}
	
				table.appendChild(tbody);
				tagCloud.appendChild(table);
			}
		};
	
		var refreshUi = mxUtils.bind(this, function(sender, evt)
		{
			if (isEnabled())
			{
				var tags = graph.getAllTags();
	
				for (var i = 0; i < tags.length; i++)
				{
					if (mxUtils.indexOf(allTags, tags[i]) < 0)
					{
						allTags.push(tags[i]);
					}
				}
	
				allTags.sort();
	
				if (graph.isSelectionEmpty())
				{
					refreshTags(allTags);
				}
				else
				{
					refreshTags(allTags, graph.getCommonTagsForCells(
						graph.getSelectionCells()));
				}
			}
		});
	
		graph.selectionModel.addListener(mxEvent.CHANGE, refreshUi);
		graph.model.addListener(mxEvent.CHANGE, refreshUi);
		graph.addListener(mxEvent.REFRESH, refreshUi);
	
		var footer = document.createElement('div');
		footer.className = 'geToolbarContainer geDialogToolbar';
		footer.style.position = 'absolute';
		footer.style.display = 'flex';
		footer.style.bottom = '0px';
		footer.style.left = '0px';
		footer.style.right = '0px';
		footer.style.height = '32px';
		footer.style.overflow = 'hidden';
		footer.style.padding = '3px 4px 4px 4px';
		footer.style.borderWidth = '1px 0px 0px 0px';
		footer.style.borderStyle = 'solid';
		footer.style.whiteSpace = 'nowrap';

		if (graph.isEnabled())
		{
			tagCloud.style.bottom = '32px';

			var link = document.createElement('a');
			link.className = 'geButton';

			var resetLink = link.cloneNode(false);
			resetLink.style.backgroundImage = 'url(' + Editor.visibleImage + ')';
			resetLink.setAttribute('title', mxResources.get('reset'));

			mxEvent.addListener(resetLink, 'click', function(evt)
			{
				graph.setHiddenTags([]);
	
				if (!mxEvent.isShiftDown(evt))
				{
					allTags = graph.hiddenTags.slice();
				}
	
				removeInvisibleSelectionCells();
				graph.refresh();
				mxEvent.consume(evt);
			});

			footer.appendChild(resetLink);
			
			if (addFn != null)
			{
				var addLink = link.cloneNode(false);
				addLink.style.backgroundImage = 'url(' + Editor.plusImage + ')';
				addLink.setAttribute('title', mxResources.get('add'));
			
				mxEvent.addListener(addLink, 'click', function(evt)
				{
					// Takes all tags and callback to update all tags
					addFn(allTags, function(newAllTags)
					{
						allTags = newAllTags;
						refreshUi();
					});

					mxEvent.consume(evt);
				});

				footer.appendChild(addLink);
			}
			
			div.appendChild(footer);
		}

		if (helpButton != null)
		{
			footer.appendChild(helpButton);
		}

		return {div: div, refresh: refreshUi};
	};
	
	/**
	 * Returns all custom fonts (old and new).
	 */
	Graph.prototype.getCustomFonts = function()
	{
		var fonts = this.extFonts;
		
		if (fonts != null)
		{
			fonts = fonts.slice();
		}
		else
		{
			fonts = [];
		}
		
		for (var key in Graph.customFontElements)
		{
			var font = Graph.customFontElements[key];
			fonts.push({name: font.name, url: font.url});
		}
		
		return fonts;
	};
	
	/**
	 * Assigns the given custom font to the selected text.
	 */
	Graph.prototype.setFont = function(name, url)
	{
		// Adds the font element to the document
		Graph.addFont(name, url);

		// Marks the element with a random font name so
		// that it can be found in the code below
		var temp = Editor.guid();
		document.execCommand('fontname', false, temp);

		// Finds the new or updated element and changes or
		// removes is data-font-src attribute as required
		var fonts = this.cellEditor.textarea.getElementsByTagName('font');
		var found = false;

		for (var i = 0; i < fonts.length; i++)
		{
			if (fonts[i].getAttribute('face') == temp)
			{
				found = true;
				fonts[i].setAttribute('face', name);

				if (url != null)
				{
					fonts[i].setAttribute('data-font-src', url);
				}
				else
				{
					fonts[i].removeAttribute('data-font-src');
				}
			}
		}

		if (!found)
		{
			// Fallback to use the real font name if a new
			// or updated element can not be found above
			document.execCommand('fontname', false, name);
		}
	};
	
	/**
	 * Disables fast zoom with shadow in lightbox for Safari
	 * to work around blank output on retina screen.
	 */
	var graphIsFastZoomEnabled = Graph.prototype.isFastZoomEnabled;
	
	Graph.prototype.isFastZoomEnabled = function()
	{
		return graphIsFastZoomEnabled.apply(this, arguments) && (!this.shadowVisible || !mxClient.IS_SF);
	};
	
	/**
	 * Updates the global variables from the vars URL parameter.
	 */
	Graph.prototype.updateGlobalUrlVariables = function()
	{
		this.globalVars = Editor.globalVars;
		
		if (urlParams['vars'] != null)
		{
			try
			{
				this.globalVars = (this.globalVars != null) ? mxUtils.clone(this.globalVars) : {};
				var vars = JSON.parse(decodeURIComponent(urlParams['vars']));
				
				if (vars != null)
				{
					for (var key in vars)
					{
						this.globalVars[key] = vars[key];
					}
				}
			}
			catch (e)
			{
				if (window.console != null)
				{
					console.log('Error in vars URL parameter: ' + e);
				}
			}
		}
	};

	/**
	 * Returns all global variables used for export. This function never returns null.
	 * This can be overridden by plugins to return global variables for export.
	 */
	Graph.prototype.getExportVariables = function()
	{
		return (this.globalVars != null) ? mxUtils.clone(this.globalVars) : {};
	};
	
	/**
	 * Adds support for vars URL parameter.
	 */
	var graphGetGlobalVariable = Graph.prototype.getGlobalVariable;
	
	Graph.prototype.getGlobalVariable = function(name)
	{
		var val = graphGetGlobalVariable.apply(this, arguments);
		
		if (val == null && this.globalVars != null)
		{
			val = this.globalVars[name];
		}
		
		return val;
	};

	/**
	 * Cached default stylesheet for image export in dark mode.
	 */
	Graph.prototype.getDefaultStylesheet = function()
	{
		if (this.defaultStylesheet == null)
		{
			var node = this.themes['default-style2'];
			var dec = new mxCodec(node.ownerDocument);
			this.defaultStylesheet = dec.decode(node);
		}
		
		return this.defaultStylesheet;
	};
	
	/**
	 * Overiddes function to use url parameter
	 */
	Graph.prototype.isViewer = function()
	{
		return urlParams['viewer'];
	};

	/**
	 * Temporarily overrides stylesheet during image export in dark mode.
	 */
	var graphGetSvg = Graph.prototype.getSvg;
	
	Graph.prototype.getSvg = function(background, scale, border, nocrop, crisp,
		ignoreSelection, showText, imgExport, linkTarget, hasShadow,
		incExtFonts, theme, exportType, cells, noCssClass, disableLinks)
	{
		var result = graphGetSvg.apply(this, arguments);

		if (theme != null)
		{
			// Uses style attribute to bypass removing fallback styles
			var style = result.getAttribute('style');
			
			if (style == null)
			{
				style = '';
			}

			var theme = (theme == 'auto' || this.getAdaptiveColors() == 'none') ?
				((this.getAdaptiveColors() == 'none') ?
					'light' : 'light dark') : theme;
			style += ' color-scheme: ' + theme + ';';
			result.setAttribute('style', style);
		}
		
		var extFonts = this.getCustomFonts();
		
		// Adds external fonts
		if (incExtFonts && extFonts.length > 0)
		{
			var svgDoc = result.ownerDocument;
			var style = (svgDoc.createElementNS != null) ?
		    	svgDoc.createElementNS(mxConstants.NS_SVG, 'style') : svgDoc.createElement('style');
			(svgDoc.setAttributeNS != null) ? style.setAttributeNS('type', 'text/css') :
				style.setAttribute('type', 'text/css');
			
			var prefix = '';
			var postfix = '';
			    	
			for (var i = 0; i < extFonts.length; i++)
			{
				var fontName = extFonts[i].name, fontUrl = extFonts[i].url;
				
				if (Graph.isCssFontUrl(fontUrl))
				{
					prefix += '@import url(' + Graph.rewriteGoogleFontUrl(fontUrl) + ');\n';
				}
				else
				{
					postfix += '@font-face {\n' +
			            'font-family: "' + fontName + '";\n' + 
			            'src: url("' + fontUrl + '");\n}\n';
				}				
			}
			
			style.appendChild(svgDoc.createTextNode(prefix + postfix));
			result.getElementsByTagName('defs')[0].appendChild(style);
		}

		// Converts background pages to subtrees
		if (Editor.replaceSvgDataUris && this.backgroundImage != null &&
			this.backgroundImage.originalSrc != null)
		{
			EditorUi.embedSvgImages(result);
		}

		// SVG element must be added to DOM for MathJax to work
		if (this.mathEnabled)
		{
			document.body.appendChild(result);
			Editor.MathJaxRender(result);
			result.parentNode.removeChild(result);

			// Copies MathJax CSS to output
			var style = result.ownerDocument.getElementById('MJX-SVG-styles');

			if (style != null)
			{
				result.getElementsByTagName('defs')[0].appendChild(style.cloneNode(true));
			}
		}
		
		return result;
	};
	
	/**
	 * Overridden to destroy the shape number.
	 */
	var cellRendererDestroy = mxCellRenderer.prototype.destroy;
	mxCellRenderer.prototype.destroy = function(state)
	{
		cellRendererDestroy.apply(this, arguments);
		
		if (state.secondLabel != null)
		{
			state.secondLabel.destroy();
			state.secondLabel = null;
		}
	};
	
	/**
	 * Includes the shape number in the return value.
	 */
	mxCellRenderer.prototype.getShapesForState = function(state)
	{
		return [state.shape, state.text, state.secondLabel, state.control];
	};

	/**
	 * Resets the global shape counter.
	 */
	var graphViewResetValidationState = mxGraphView.prototype.resetValidationState;
	
	mxGraphView.prototype.resetValidationState = function()
	{
		graphViewResetValidationState.apply(this, arguments);
		this.enumerationState = 0;
	};
	
	/**
	 * Adds shape number update the validation step.
	 */
	var graphViewStateValidated = mxGraphView.prototype.stateValidated;
	
	mxGraphView.prototype.stateValidated = function(state)
	{
		if (state.shape != null)
		{
			this.redrawEnumerationState(state);
		}

		return graphViewStateValidated.apply(this, arguments);
	};

	/**
	 * Returns the markup to be used for the enumeration shape.
	 */
	mxGraphView.prototype.createEnumerationValue = function(state)
	{
		var value = decodeURIComponent(mxUtils.getValue(state.style, 'enumerateValue', ''));

		if (value == '')
		{
			value = ++this.enumerationState;
		}

		return '<div style="padding:2px;border:1px solid gray;background:yellow;border-radius:2px;">' +
			mxUtils.htmlEntities(value) + '</div>';
	};

	/**
	 * Adds drawing and update of the shape number.
	 */
	mxGraphView.prototype.redrawEnumerationState = function(state)
	{
		var enumerate = mxUtils.getValue(state.style, 'enumerate', 0) == '1';

		if (enumerate && state.secondLabel == null)
		{
			state.secondLabel = new mxText('', new mxRectangle(),
				mxConstants.ALIGN_LEFT, mxConstants.ALIGN_BOTTOM);
			state.secondLabel.size = 12;
			state.secondLabel.state = state;
			state.secondLabel.dialect = mxConstants.DIALECT_STRICTHTML;

			this.graph.cellRenderer.initializeLabel(state, state.secondLabel);
		}
		else if (!enumerate && state.secondLabel != null)
		{
			state.secondLabel.destroy();
			state.secondLabel = null;
		}

		var shape = state.secondLabel;

		if (shape != null)
		{
			var s = state.view.scale;
			var value = this.createEnumerationValue(state);
			var bounds = this.graph.model.isVertex(state.cell) ?
				new mxRectangle(state.x + state.width - 4 * s, state.y + 4 * s, 0, 0) :
				mxRectangle.fromPoint(state.view.getPoint(state));

			if (!shape.bounds.equals(bounds) || shape.value != value || shape.scale != s)
			{
				shape.bounds = bounds;
				shape.value = value;
				shape.scale = s;
				shape.redraw();
			}
		}
	};

	/**
	 * Updates the SVG for the background image if it references another page.
	 */
	var graphRefresh = Graph.prototype.refresh;
	Graph.prototype.refresh = function()
	{
		this.refreshBackgroundImage();
		graphRefresh.apply(this, arguments);
	};
				 
	/**
	 * Updates the SVG for the background image if it references another page.
	 */
	Graph.prototype.refreshBackgroundImage = function()
	{
		if (this.backgroundImage != null && this.backgroundImage.originalSrc != null)
		{
			this.setBackgroundImage(this.backgroundImage);
			this.view.validateBackgroundImage();
		}
	};
	
	/**
	 * Sets default style (used in editor.get/setGraphXml below)
	 */
	var graphLoadStylesheet = Graph.prototype.loadStylesheet;
	
	Graph.prototype.loadStylesheet = function()
	{
		graphLoadStylesheet.apply(this, arguments);
		this.currentStyle = 'default-style2';
	};

	/**
	 * Adds support for data:action/json,{"actions":[actions]} where actions is
	 * a comma-separated list of JSON objects.
	 * 
	 * An example action is:
	 * 
	 * data:action/json,{"actions":[{"toggle": {"cells": ["3", "4"]}}]}
	 * 
	 * This toggles the visible state of the cells with ID 3 and 4.
	 */
	Graph.prototype.handleCustomLink = function(href, cell)
	{
		if (href.substring(0, 17) == 'data:action/json,')
		{
			var link = JSON.parse(href.substring(17));

			if (link.actions != null)
			{
				this.executeCustomActions(link.actions, null, cell);
			}
		}
	};
		
	/**
	 * Runs the given actions and invokes done when all actions have been executed.
	 * When adding new actions that reference cell IDs support for updating
	 * those cell IDs must be handled in Graph.updateCustomLinkActions
	 */
	Graph.prototype.executeCustomActions = function(actions, done, cell)
	{
		if (!this.executingCustomActions)
		{
			this.executingCustomActions = true;
			var updatingModel = false;
			var waitCounter = 0;
			var index = 0;

			var beginUpdate = mxUtils.bind(this, function()
			{
				if (!updatingModel)
				{
					updatingModel = true;
					this.model.beginUpdate();
				}
			});

			var endUpdate = mxUtils.bind(this, function()
			{
				if (updatingModel)
				{
					updatingModel = false;
					this.model.endUpdate();
				}
			});

			var waitAndExecute = mxUtils.bind(this, function()
			{
				if (waitCounter > 0)
				{
					waitCounter--;
				}

				if (waitCounter == 0)
				{
					executeNextAction()
				}
			});

			var executeNextAction = mxUtils.bind(this, function()
			{
				if (index < actions.length)
				{
					var stop = this.stoppingCustomActions;
					var action = actions[index++];
					var animations = [];

					// Executes open actions before starting transaction
					if (action.open != null)
					{
						endUpdate();

						if (this.isCustomLink(action.open))
						{
							if (!this.customLinkClicked(action.open, cell))
							{
								return;
							}
						}
						else
						{
							this.openLink(action.open);
						}
					}

					if (action.wait != null && !stop)
					{
						this.pendingExecuteNextAction = mxUtils.bind(this, function()
						{
							this.pendingExecuteNextAction = null;
							this.pendingWaitThread = null;
							waitAndExecute();
						});

						waitCounter++;
						this.pendingWaitThread = window.setTimeout(this.pendingExecuteNextAction,
							(action.wait != '') ? parseInt(action.wait) : 1000);
						endUpdate();
					}

					if (action.opacity != null && action.opacity.value != null)
					{
						Graph.setOpacityForNodes(this.getNodesForCells(
							this.getCellsForAction(action.opacity, true)),
							action.opacity.value);
					}

					if (action.fadeIn != null)
					{
						waitCounter++;
						Graph.fadeNodes(this.getNodesForCells(
							this.getCellsForAction(action.fadeIn, true)),
							0, 1, waitAndExecute, (stop) ?
							0 : action.fadeIn.delay);
					}

					if (action.fadeOut != null)
					{
						waitCounter++;
						Graph.fadeNodes(this.getNodesForCells(
							this.getCellsForAction(action.fadeOut, true)),
							1, 0, waitAndExecute, (stop) ?
							0 : action.fadeOut.delay);
					}

					if (action.wipeIn != null)
					{
						animations = animations.concat(this.createWipeAnimations(
							this.getCellsForAction(action.wipeIn, true), true));
					}

					if (action.wipeOut != null)
					{
						animations = animations.concat(this.createWipeAnimations(
							this.getCellsForAction(action.wipeOut, true), false));
					}

					if (action.popIn != null)
					{
						animations = animations.concat(this.createPopAnimations(
							this.getCellsForAction(action.popIn, true), true));
					}

					if (action.popOut != null)
					{
						animations = animations.concat(this.createPopAnimations(
							this.getCellsForAction(action.popOut, true), false));
					}

					// Executes all actions that change cell states
					if (action.toggle != null)
					{
						beginUpdate();
						this.toggleCells(this.getCellsForAction(action.toggle, true));
					}

					if (action.show != null)
					{
						beginUpdate();
						var temp = this.getCellsForAction(action.show, true);
						Graph.setOpacityForNodes(this.getNodesForCells(temp), 1);
						this.setCellsVisible(temp, true);
					}

					if (action.hide != null)
					{
						beginUpdate();
						var temp = this.getCellsForAction(action.hide, true);
						Graph.setOpacityForNodes(this.getNodesForCells(temp), 0);
						this.setCellsVisible(temp, false);
					}
					
					if (action.toggleStyle != null && action.toggleStyle.key != null)
					{
						beginUpdate();
						this.toggleCellStyles(action.toggleStyle.key, (action.toggleStyle.defaultValue != null) ?
							action.toggleStyle.defaultValue : '0', this.getCellsForAction(action.toggleStyle, true));
					}

					if (action.style != null && action.style.key != null)
					{
						beginUpdate();
						this.setCellStyles(action.style.key, action.style.value,
							this.getCellsForAction(action.style, true));
					}

					// Executes stateless actions on cells
					var cells = [];
						
					if (action.select != null && this.isEnabled())
					{
						cells = this.getCellsForAction(action.select);
						this.setSelectionCells(cells);
					}

					if (action.highlight != null)
					{
						cells = this.getCellsForAction(action.highlight);
						this.highlightCells(cells, action.highlight.color,
							action.highlight.duration,
							action.highlight.opacity);
					}

					if (action.scroll != null)
					{
						cells = this.getCellsForAction(action.scroll);
					}
					
					if (action.viewbox != null)
					{
						this.fitWindow(action.viewbox, action.viewbox.border);
					}
					
					if (cells.length > 0)
					{
						this.scrollCellToVisible(cells[0]);
					}
					
					if (cell != null && action.explore != null)
					{
						Graph.exploreFromCell(this, cell, action.explore);
					}

					if (action.tags != null)
					{
						if (action.tags.toggle != null)
						{
							var tags = action.tags.toggle;

							if (tags.length == 0)
							{
								tags = this.getAllTags();
							}

							for (var i = 0; i < tags.length; i++)
							{
								this.toggleHiddenTag(tags[i]);
							}
						}
						
						var hidden = null;

						if (action.tags.hidden != null)
						{
							if (hidden == null)
							{
								hidden = [];
							}
							
							hidden = hidden.concat(action.tags.hidden);
						}

						if (action.tags.visible != null)
						{
							if (hidden == null)
							{
								hidden = [];
							}

							var all = this.getAllTags();

							for (var i = 0; i < all.length; i++)
							{
								if (mxUtils.indexOf(action.tags.visible, all[i]) < 0 &&
									mxUtils.indexOf(hidden, all[i]) < 0)
								{
									hidden.push(all[i]);
								}
							}
						}

						if (hidden != null)
						{
							this.setHiddenTags(hidden);
						}

						this.refresh();
					}

					if (animations.length > 0)
					{
						waitCounter++;
						this.executeAnimations(animations, waitAndExecute,
							(stop) ? 1 : action.steps,
							(stop) ? 0 : action.delay);
					}

					if (waitCounter == 0)
					{
						executeNextAction();
					}
					else
					{
						endUpdate();
					}
				}
				else
				{
					this.executingCustomActions = false;
					this.stoppingCustomActions = false;
					endUpdate();

					if (done != null)
					{
						done();
					}
				}
			});

			executeNextAction();
		}
		else
		{
			this.stoppingCustomActions = true;

			if (this.pendingWaitThread != null)
			{
				window.clearTimeout(this.pendingWaitThread);
			}

			if (this.pendingExecuteNextAction != null)
			{
				this.pendingExecuteNextAction();
			}

			this.fireEvent(new mxEventObject('stopExecutingCustomActions'));
		}
	};

	/**
	 * Updates cell IDs in custom links on the given cell and its label.
	 */
	Graph.prototype.doUpdateCustomLinksForCell = function(mapping, cell)
	{
		var href = this.getLinkForCell(cell);
		
		if (href != null && href.substring(0, 17) == 'data:action/json,')
		{
			this.setLinkForCell(cell, this.updateCustomLink(mapping, href));
		}
		
		if (this.isHtmlLabel(cell))
		{
			var temp = document.createElement('div');
			temp.innerHTML = Graph.sanitizeHtml(this.getLabel(cell));
			var links = temp.getElementsByTagName('a');
			var changed = false;
			
			for (var i = 0; i < links.length; i++)
			{
				href = links[i].getAttribute('href');
				
				if (href != null && href.substring(0, 17) == 'data:action/json,')
				{
					links[i].setAttribute('href', this.updateCustomLink(mapping, href));
					changed = true;
				}
			}
			
			if (changed)
			{
				this.labelChanged(cell, temp.innerHTML);
			}
		}
	};
	
	/**
	 * Updates cell IDs in the given custom link and returns the updated link.
	 */
	Graph.prototype.updateCustomLink = function(mapping, href)
	{
		if (href.substring(0, 17) == 'data:action/json,')
		{
			try
			{
				var link = JSON.parse(href.substring(17));

				if (link.actions != null)
				{
					this.updateCustomLinkActions(mapping, link.actions);
					href = 'data:action/json,' + JSON.stringify(link);
				}
			}
			catch (e)
			{
				// Ignore
			}
		}
		
		return href;
	};

	/**
	 * Updates cell IDs in the given custom link actions.
	 */
	Graph.prototype.updateCustomLinkActions = function(mapping, actions)
	{
		for (var i = 0; i < actions.length; i++)
		{
			var action = actions[i];

			for (var name in action)
			{
				this.updateCustomLinkAction(mapping, action[name], 'cells');
				this.updateCustomLinkAction(mapping, action[name], 'excludeCells');
			}
		}
	};
	
	/**
	 * Updates cell IDs in the given custom link action.
	 */
	Graph.prototype.updateCustomLinkAction = function(mapping, action, name)
	{
		if (action != null && action[name] != null)
		{
			var result = [];
			
			for (var i = 0; i < action[name].length; i++)
			{
				if (action[name][i] == '*')
				{
					result.push(action[name][i]);
				}
				else
				{
					var temp = mapping[action[name][i]];
					
					if (temp != null)
					{
						if (temp != '')
						{
							result.push(temp);
						}
					}
					else
					{
						result.push(action[name][i]);
					}
				}
			}
			
			action[name] = result;
		}
	};
	
	/**
	 * Handles each action in the action array of a custom link. This code
	 * handles toggle actions for cell IDs.
	 */
	Graph.prototype.getCellsForAction = function(action, layers)
	{
		var result = this.getCellsById(action.cells).concat(
			this.getCellsForTags(action.tags, null, layers));

		// Removes excluded cells
		if (action.excludeCells != null)
		{
			var temp = [];

			for (var i = 0; i < result.length; i++)
			{
				if (action.excludeCells.indexOf(result[i].id) < 0)
				{
					temp.push(result[i]);
				}
			}

			result = temp;
		}

		return result;
	};
	
	/**
	 * Returns the cells in the model (or given array) that have all of the
	 * given tags in their tags property.
	 */
	Graph.prototype.getCellsById = function(ids)
	{
		var result = [];
		
		if (ids != null)
		{
			for (var i = 0; i < ids.length; i++)
			{
				if (ids[i] == '*')
				{
					var parent = this.model.getRoot();
					
					result = result.concat(this.model.filterDescendants(function(cell)
					{
						return cell != parent;
					}, parent));
				}
				else
				{
					var cell = this.model.getCell(ids[i]);
					
					if (cell != null)
					{
						result.push(cell);
					}
				}
			}
		}
		
		return result;
	};

	/**
	 * Adds support for custom fonts in cell styles.
	 */
	var graphIsCellVisible = Graph.prototype.isCellVisible;
	Graph.prototype.isCellVisible = function(cell)
	{
		return graphIsCellVisible.apply(this, arguments) &&
			!this.isAllTagsHidden(this.getTagsForCell(cell));
	};
  
	/**
	 * Returns the tags for the given cell as a string.
	 */
	Graph.prototype.setHiddenTags = function(tags)
	{
		this.hiddenTags = tags;
		this.fireEvent(new mxEventObject('hiddenTagsChanged'));
	};

	/**
	 * Returns the tags for the given cell as a string.
	 */
	Graph.prototype.toggleHiddenTag = function(tag)
	{
		var idx = mxUtils.indexOf(this.hiddenTags, tag);
									
		if (idx < 0)
		{
			this.hiddenTags.push(tag);
		}
		else if (idx >= 0)
		{
			this.hiddenTags.splice(idx, 1);
		}
		
		this.fireEvent(new mxEventObject('hiddenTagsChanged'));
	};
 
	/**
	 * Returns the cells in the model (or given array) that have all of the
	 * given tags in their tags property.
	 */
	Graph.prototype.isAllTagsHidden = function(tags)
	{
		if (tags == null || tags.length == 0 ||
			this.hiddenTags.length == 0)
		{
			return false;
		}
		else
		{
			var tmp = tags.split(' ');

			if (tmp.length > this.hiddenTags.length)
			{
				return false;
			}
			else
			{
				for (var i = 0; i < tmp.length; i++)
				{
					if (mxUtils.indexOf(this.hiddenTags, tmp[i]) < 0)
					{
						return false;
					}
				}
				
				return true;
			}
		}
	};

	/**
	 * Returns the cells in the model (or given array) that have all of the
	 * given tags in their tags property.
	 */
	Graph.prototype.getCellsForTags = function(tagList, cells, includeLayers, checkVisible)
	{
		var result = [];
		
		if (tagList != null)
		{
			cells = (cells != null) ? cells : this.model.getDescendants(this.model.getRoot());
			
			var tagCount = 0;
			var lookup = {};
			
			for (var i = 0; i < tagList.length; i++)
			{
				if (tagList[i].length > 0)
				{
					lookup[tagList[i]] = true;
					tagCount++;
				}
			}
			
			for (var i = 0; i < cells.length; i++)
			{
				if ((includeLayers && this.model.getParent(cells[i]) == this.model.root) ||
					this.model.isVertex(cells[i]) || this.model.isEdge(cells[i]))
				{
					var tags = this.getTagsForCell(cells[i]);
					var match = false;
	
					if (tags.length > 0)
					{
						var tmp = tags.split(' ');
						
						if (tmp.length >= tagList.length)
						{
							var matchCount = 0;
							
							for (var j = 0; j < tmp.length && (matchCount < tagCount); j++)
							{
								if (lookup[tmp[j]] != null)
								{
									matchCount++;
								}
							}
							
							match = matchCount == tagCount;
						}
					}
					
					if (match && ((checkVisible != true) || this.isCellVisible(cells[i])))
					{
						result.push(cells[i]);
					}
				}
			}
		}
		
		return result;
	};

	/**
	 * Returns all tags in the diagram.
	 */
	Graph.prototype.getAllTags = function()
	{
		return this.getTagsForCells(
			this.model.getDescendants(
				this.model.getRoot()));
		
	};

	/**
	 * Returns the common tags for the given cells as a array.
	 */
	Graph.prototype.getCommonTagsForCells = function(cells)
	{
		var commonTokens = null;
		var validTags = [];
		 
		for (var i = 0; i < cells.length; i++)
		{
			var tags = this.getTagsForCell(cells[i]);
			validTags = [];
 
			if (tags.length > 0)
			{
				var tokens = tags.split(' ');
				var temp = {};
				 
				for (var j = 0; j < tokens.length; j++)
				{
					if (commonTokens == null || commonTokens[tokens[j]] != null)
					{
						temp[tokens[j]] = true;
						validTags.push(tokens[j]);
					}
				}
				 
				commonTokens = temp;
			}
			else
			{
				return [];
			}
		}
	 
		return validTags;
	};
 
	/**
	 * Returns all tags for the given cells as an array.
	 */
	Graph.prototype.getTagsForCells = function(cells)
	{
		var tokens = [];
		var temp = {};
		
		for (var i = 0; i < cells.length; i++)
		{
			var tags = this.getTagsForCell(cells[i]);

			if (tags.length > 0)
			{
				var t = tags.split(' ');
				
				for (var j = 0; j < t.length; j++)
				{
					if (temp[t[j]] == null)
					{
						temp[t[j]] = true;
						tokens.push(t[j]);
					}
				}
			}
		}
		
		return tokens;
	};

	/**
	 * Returns the tags for the given cell as a string.
	 */
	Graph.prototype.getTagsForCell = function(cell)
	{
		return this.getAttributeForCell(cell, 'tags', '');
	};

	/**
	 * Adds the given array of tags to the given array cells.
	 */
	Graph.prototype.addTagsForCells = function(cells, tagList)
	{
		if (cells.length > 0 && tagList.length > 0)
		{
			this.model.beginUpdate();
			
			try
			{
				for (var i = 0; i < cells.length; i++)
				{
					var temp = this.getTagsForCell(cells[i]);
					var tags = temp.split(' ');
					var changed = false;
		
					for (var j = 0; j < tagList.length; j++)
					{
						var tag = mxUtils.trim(tagList[j]);

						if (tag != '' && mxUtils.indexOf(tags, tag) < 0)
						{
							temp = (temp.length > 0) ? temp + ' ' + tag : tag;
							changed = true;
						}
					}
					
					if (changed)
					{
						this.setAttributeForCell(cells[i], 'tags', temp);
					}
				}
			}
			finally
			{
				this.model.endUpdate();
			}
		}
	};

	/**
	 * Removes the given array of tags from the given array cells.
	 */
	Graph.prototype.removeTagsForCells = function(cells, tagList)
	{
		if (cells.length > 0 && tagList.length > 0)
		{
			this.model.beginUpdate();
			
			try
			{
				for (var i = 0; i < cells.length; i++)
				{
					var tags = this.getTagsForCell(cells[i]);
					
					if (tags.length > 0)
					{
						var tokens = tags.split(' ');
						var changed = false;
						
						for (var j = 0; j < tagList.length; j++)
						{
							var idx = mxUtils.indexOf(tokens, tagList[j]);
							
							if (idx >= 0)
							{
								tokens.splice(idx, 1);
								changed = true;
							}
						}

						if (changed)
						{
							this.setAttributeForCell(cells[i], 'tags', tokens.join(' '));
						}
					}
				}
			}
			finally
			{
				this.model.endUpdate();
			}
		}
	};
 
	/**
	 * Shows or hides the given cells.
	 */
	Graph.prototype.toggleCells = function(cells)
	{
		this.model.beginUpdate();
		try
		{
			for (var i = 0; i < cells.length; i++)
			{
				this.model.setVisible(cells[i], !this.model.isVisible(cells[i]))
			}
		}
		finally
		{
			this.model.endUpdate();
		}
	};
	
	/**
	 * Shows or hides the given cells.
	 */
	Graph.prototype.setCellsVisible = function(cells, visible)
	{
		this.model.beginUpdate();
		try
		{
			for (var i = 0; i < cells.length; i++)
			{
				this.model.setVisible(cells[i], visible);
			}
		}
		finally
		{
			this.model.endUpdate();
		}
	};
	
	/**
	 * Highlights the given cell.
	 */
	Graph.prototype.highlightCells = function(cells, color, duration, opacity)
	{
		for (var i = 0; i < cells.length; i++)
		{
			this.highlightCell(cells[i], color, duration, opacity);
		}
	};
	
	/**
	 * Highlights the given cell.
	 */
	Graph.prototype.highlightCell = function(cell, color, duration, opacity, strokeWidth)
	{
		color = (color != null) ? color : mxConstants.DEFAULT_VALID_COLOR;
		duration = (duration != null) ? duration : 1000;
		var state = this.view.getState(cell);
		var hl = null;
		
		if (state != null)
		{
			strokeWidth = (strokeWidth != null) ? strokeWidth : 4;
			var sw = Math.max(strokeWidth + 1, mxUtils.getValue(state.style,
				mxConstants.STYLE_STROKEWIDTH, 1) + strokeWidth);
			hl = new mxCellHighlight(this, color, sw, false);
			
			if (opacity != null)
			{
				hl.opacity = opacity;
			}
			
			hl.highlight(state);
			
			// Fades out the highlight after a duration
			window.setTimeout(function()
			{
				if (hl.shape != null)
				{
				 	mxUtils.setPrefixedStyle(hl.shape.node.style,
						'transition', 'all 1200ms ease-in-out');
					hl.shape.node.style.opacity = 0;
				}
				
				// Destroys the highlight after the fade
				window.setTimeout(function()
				{
					hl.destroy();
				}, 1200);
			}, duration);
		}

		return hl;
	};

	/**
	 * Adds a shadow filter to the given svg root.
	 */
	Graph.prototype.addSvgShadow = function(svgRoot, group, createOnly, extend)
	{
		createOnly = (createOnly != null) ? createOnly : false;
		extend = (extend != null) ? extend : true;
		var size = Math.ceil(this.svgShadowSize * this.svgShadowBlur);
		
		if (!createOnly)
		{
			group = (group != null) ? group : svgRoot.getElementsByTagName('g')[0];
			
			if (group != null)
			{
				this.updateShadowFilter(group, true);
				
				if (!isNaN(parseInt(svgRoot.getAttribute('width'))) && extend)
				{
					svgRoot.setAttribute('width', parseInt(svgRoot.getAttribute('width')) + size);
					svgRoot.setAttribute('height', parseInt(svgRoot.getAttribute('height')) + size);
					
					// Updates viewbox if one exists
					var vb = svgRoot.getAttribute('viewBox');
					
					if (vb != null && vb.length > 0)
					{
						var tokens = vb.split(' ');
						
						if (tokens.length > 3)
						{
							w = parseFloat(tokens[2]) + size;
							h = parseFloat(tokens[3]) + size;
							
							svgRoot.setAttribute('viewBox', tokens[0] + ' ' + tokens[1] + ' ' + w + ' ' + h);
						}
					}
				}
			}
		}

		return size;
	};
	
	/**
	 * Loads the stylesheet for this graph.
	 * LATER: Update shadow filter after zoom?
	 */
	Graph.prototype.updateShadowFilter = function(elt, visible)
	{
		if (!mxClient.IS_SF)
		{
			if (visible)
			{
				var cssColor = mxUtils.getLightDarkColor(
					this.svgShadowColor, this.svgShadowOpacity);
				elt.style.filter = 'drop-shadow(' +
					Math.round(this.svgShadowSize * 100) / 100 + 'px ' +
					Math.round(this.svgShadowSize * 100) / 100 + 'px ' +
					Math.round(this.svgShadowBlur * 100) / 100 + 'px ' +
					cssColor.cssText + ')'
			}
			else
			{
				elt.style.filter = '';
			}
		}
	};

	/**
	 * Loads the stylesheet for this graph.
	 */
	Graph.prototype.setShadowVisible = function(value, fireEvent)
	{
		fireEvent = (fireEvent != null) ? fireEvent : true;
		this.shadowVisible = value;
		this.updateShadowFilter(this.view.getDrawPane(), this.shadowVisible);
		
		if (fireEvent)
		{
			this.fireEvent(new mxEventObject('shadowVisibleChanged'));
		}
	};
	
	/**
	 * Selects first unlocked layer if one exists
	 */
	Graph.prototype.checkDefaultParent = function()
	{
		if (this.defaultParent != null &&
			!this.model.contains(this.defaultParent))
		{
			this.setDefaultParent(null);
			this.selectUnlockedLayer();
		}
	};
	
	/**
	 * Selects first unlocked layer if one exists
	 */
	Graph.prototype.selectUnlockedLayer = function()
	{
		if (this.defaultParent == null)
		{
			var childCount = this.model.getChildCount(this.model.root);
			var cell = null;
			var index = 0;
			
			do
			{
				cell = this.model.getChildAt(this.model.root, index);
			} while (index++ < childCount && mxUtils.getValue(this.getCellStyle(cell), 'locked', '0') == '1')
			
			if (cell != null)
			{
				this.setDefaultParent(cell);
			}
		}
	};

	/**
	 * Specifies special libraries that are loaded via dynamic JS. Add cases
	 * where the filename cannot be worked out from the package name. The
	 * standard scheme for this mapping is stencils/packagename.xml. If there
	 * are multiple XML files, any JS files or any anomalies in the filename or
	 * directory that contains the file, then an entry must be added here and
	 * in EmbedServlet2 for the loading of the shapes to work.
	 */
	// Required to avoid 404 for mockup.xml since naming of mxgraph.mockup.anchor does not contain
	// buttons even though it is defined in the mxMockupButtons.js file. This could only be fixed
	// with aliases for existing shapes or aliases for basenames, but this is essentially the same.
	mxStencilRegistry.libraries['mockup'] = [SHAPES_PATH + '/mockup/mxMockupButtons.js'];
	
	mxStencilRegistry.libraries['arrows2'] = [SHAPES_PATH + '/mxArrows.js'];
	mxStencilRegistry.libraries['atlassian'] = [STENCIL_PATH + '/atlassian.xml', SHAPES_PATH + '/mxAtlassian.js'];
	mxStencilRegistry.libraries['bpmn'] = [SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/bpmn.xml', SHAPES_PATH + '/bpmn/mxBpmnShape2.js'];
	mxStencilRegistry.libraries['bpmn2'] = [SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/bpmn.xml', SHAPES_PATH + '/bpmn/mxBpmnShape2.js'];
	mxStencilRegistry.libraries['c4'] = [SHAPES_PATH + '/mxC4.js'];
	mxStencilRegistry.libraries['cisco19'] = [SHAPES_PATH + '/mxCisco19.js', STENCIL_PATH + '/cisco19.xml'];
	mxStencilRegistry.libraries['cisco_safe'] = [SHAPES_PATH + '/mxCiscoSafe.js', STENCIL_PATH + '/cisco_safe/architecture.xml', STENCIL_PATH + '/cisco_safe/business_icons.xml', STENCIL_PATH + '/cisco_safe/capability.xml', STENCIL_PATH + '/cisco_safe/design.xml', STENCIL_PATH + '/cisco_safe/iot_things_icons.xml', STENCIL_PATH + '/cisco_safe/people_places_things_icons.xml', STENCIL_PATH + '/cisco_safe/security_icons.xml', STENCIL_PATH + '/cisco_safe/technology_icons.xml', STENCIL_PATH + '/cisco_safe/threat.xml'];
	mxStencilRegistry.libraries['dfd'] = [SHAPES_PATH + '/mxDFD.js'];
	mxStencilRegistry.libraries['er'] = [SHAPES_PATH + '/er/mxER.js'];
	mxStencilRegistry.libraries['kubernetes'] = [SHAPES_PATH + '/mxKubernetes.js', STENCIL_PATH + '/kubernetes.xml', STENCIL_PATH + '/kubernetes2.xml'];
	mxStencilRegistry.libraries['flowchart'] = [SHAPES_PATH + '/mxFlowchart.js', STENCIL_PATH + '/flowchart.xml'];
	mxStencilRegistry.libraries['ios'] = [SHAPES_PATH + '/mockup/mxMockupiOS.js'];
	mxStencilRegistry.libraries['rackGeneral'] = [SHAPES_PATH + '/rack/mxRack.js', STENCIL_PATH + '/rack/general.xml'];
	mxStencilRegistry.libraries['rackF5'] = [STENCIL_PATH + '/rack/f5.xml'];
	mxStencilRegistry.libraries['lean_mapping'] = [SHAPES_PATH + '/mxLeanMap.js', STENCIL_PATH + '/lean_mapping.xml'];
	mxStencilRegistry.libraries['basic'] = [SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/basic.xml'];
	mxStencilRegistry.libraries['ios7icons'] = [STENCIL_PATH + '/ios7/icons.xml'];
	mxStencilRegistry.libraries['ios7ui'] = [SHAPES_PATH + '/ios7/mxIOS7Ui.js', STENCIL_PATH + '/ios7/misc.xml'];
	mxStencilRegistry.libraries['android'] = [SHAPES_PATH + '/mxAndroid.js', STENCIL_PATH + '/android/android.xml'];
	mxStencilRegistry.libraries['electrical/abstract'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/abstract.xml'];
	mxStencilRegistry.libraries['electrical/logic_gates'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/logic_gates.xml'];
	mxStencilRegistry.libraries['electrical/miscellaneous'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/miscellaneous.xml'];
	mxStencilRegistry.libraries['electrical/signal_sources'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/signal_sources.xml'];
	mxStencilRegistry.libraries['electrical/electro-mechanical'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/electro-mechanical.xml'];
	mxStencilRegistry.libraries['electrical/transmission'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/transmission.xml'];
	mxStencilRegistry.libraries['infographic'] = [SHAPES_PATH + '/mxInfographic.js'];
	mxStencilRegistry.libraries['mockup/buttons'] = [SHAPES_PATH + '/mockup/mxMockupButtons.js'];
	mxStencilRegistry.libraries['mockup/containers'] = [SHAPES_PATH + '/mockup/mxMockupContainers.js'];
	mxStencilRegistry.libraries['mockup/forms'] = [SHAPES_PATH + '/mockup/mxMockupForms.js'];
	mxStencilRegistry.libraries['mockup/graphics'] = [SHAPES_PATH + '/mockup/mxMockupGraphics.js', STENCIL_PATH + '/mockup/misc.xml'];
	mxStencilRegistry.libraries['mockup/markup'] = [SHAPES_PATH + '/mockup/mxMockupMarkup.js'];
	mxStencilRegistry.libraries['mockup/misc'] = [SHAPES_PATH + '/mockup/mxMockupMisc.js', STENCIL_PATH + '/mockup/misc.xml'];
	mxStencilRegistry.libraries['mockup/navigation'] = [SHAPES_PATH + '/mockup/mxMockupNavigation.js', STENCIL_PATH + '/mockup/misc.xml'];
	mxStencilRegistry.libraries['mockup/text'] = [SHAPES_PATH + '/mockup/mxMockupText.js'];
	mxStencilRegistry.libraries['floorplan'] = [SHAPES_PATH + '/mxFloorplan.js', STENCIL_PATH + '/floorplan.xml'];
	mxStencilRegistry.libraries['bootstrap'] = [SHAPES_PATH + '/mxBootstrap.js', SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/bootstrap.xml'];
	mxStencilRegistry.libraries['gmdl'] = [SHAPES_PATH + '/mxGmdl.js', STENCIL_PATH + '/gmdl.xml'];
	mxStencilRegistry.libraries['gcp2'] = [SHAPES_PATH + '/mxGCP2.js', STENCIL_PATH + '/gcp2.xml'];
	mxStencilRegistry.libraries['ibm'] = [SHAPES_PATH + '/mxIBM.js', STENCIL_PATH + '/ibm.xml'];
	mxStencilRegistry.libraries['ibmcloud'] = [STENCIL_PATH + '/ibm_cloud.xml'];
	mxStencilRegistry.libraries['cabinets'] = [SHAPES_PATH + '/mxCabinets.js', STENCIL_PATH + '/cabinets.xml'];
	mxStencilRegistry.libraries['archimate'] = [SHAPES_PATH + '/mxArchiMate.js'];
	mxStencilRegistry.libraries['archimate3'] = [SHAPES_PATH + '/mxArchiMate3.js'];
	mxStencilRegistry.libraries['sysml'] = [SHAPES_PATH + '/mxSysML.js'];
	mxStencilRegistry.libraries['eip'] = [SHAPES_PATH + '/mxEip.js', STENCIL_PATH + '/eip.xml'];
	mxStencilRegistry.libraries['networks'] = [SHAPES_PATH + '/mxNetworks.js', STENCIL_PATH + '/networks.xml'];
	mxStencilRegistry.libraries['networks2'] = [SHAPES_PATH + '/mxNetworks2.js', STENCIL_PATH + '/networks2.xml'];
	mxStencilRegistry.libraries['aws3d'] = [SHAPES_PATH + '/mxAWS3D.js', STENCIL_PATH + '/aws3d.xml'];
	mxStencilRegistry.libraries['aws4'] = [SHAPES_PATH + '/mxAWS4.js', STENCIL_PATH + '/aws4.xml'];
	mxStencilRegistry.libraries['aws4b'] = [SHAPES_PATH + '/mxAWS4.js', STENCIL_PATH + '/aws4.xml'];
	mxStencilRegistry.libraries['uml25'] = [SHAPES_PATH + '/mxUML25.js'];
	mxStencilRegistry.libraries['veeam'] = [STENCIL_PATH + '/veeam/2d.xml', STENCIL_PATH + '/veeam/3d.xml', STENCIL_PATH + '/veeam/veeam.xml'];
	mxStencilRegistry.libraries['veeam2'] = [STENCIL_PATH + '/veeam/2d.xml', STENCIL_PATH + '/veeam/3d.xml', STENCIL_PATH + '/veeam/veeam2.xml'];
	mxStencilRegistry.libraries['pid2inst'] = [SHAPES_PATH + '/pid2/mxPidInstruments.js'];
	mxStencilRegistry.libraries['pid2misc'] = [SHAPES_PATH + '/pid2/mxPidMisc.js', STENCIL_PATH + '/pid/misc.xml'];
	mxStencilRegistry.libraries['pid2valves'] = [SHAPES_PATH + '/pid2/mxPidValves.js'];
	mxStencilRegistry.libraries['pidFlowSensors'] = [STENCIL_PATH + '/pid/flow_sensors.xml'];
	mxStencilRegistry.libraries['salesforce'] = [STENCIL_PATH + '/salesforce.xml'];
	mxStencilRegistry.libraries['sap'] = [SHAPES_PATH + '/mxSAP.js', STENCIL_PATH + '/sap.xml'];
	mxStencilRegistry.libraries['emoji'] = [SHAPES_PATH + '/emoji/mxEmoji.js'];

	// Triggers dynamic loading for markers
	mxMarker.getPackageForType = function(type)
	{
		var name = null;
		
		if (type != null && type.length > 0)
		{
			if (type.substring(0, 2) == 'ER')
			{
				name = 'mxgraph.er';
			}
			else if (type.substring(0, 5) == 'sysML')
			{
				name = 'mxgraph.sysml';
			}
		}
		
		return name;
	};
	
	var mxMarkerCreateMarker = mxMarker.createMarker;
	
	mxMarker.createMarker = function(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled)
	{
		if (type != null)
		{
			var f = mxMarker.markers[type];
			
			if (f == null)
			{
				var name = this.getPackageForType(type);
				
				if (name != null)
				{
					mxStencilRegistry.getStencil(name);
				}
			}
		}
		
		return mxMarkerCreateMarker.apply(this, arguments);
	};

	/**
	 * Adds style to mark stencils as lines.
	 */
	var mxStencilDrawShape = mxStencil.prototype.drawShape;
		
	mxStencil.prototype.drawShape = function(canvas, shape, x, y, w, h)
	{
		if (mxUtils.getValue(shape.style, 'lineShape', null) == '1')
		{
			canvas.setFillColor(mxUtils.getValue(shape.style,
				mxConstants.STYLE_STROKECOLOR, this.stroke));
		}

		return mxStencilDrawShape.apply(this, arguments);
	};

	/**
	 * Constructs a new print dialog.
	 */
	PrintDialog.prototype.create = function(editorUi, titleText, fn, btnTitle)
	{
		var graph = editorUi.editor.graph;
		var div = document.createElement('div');

		var title = document.createElement('h3');
		title.style.width = '100%';
		title.style.textAlign = 'center';
		title.style.marginTop = '0px';
		title.style.marginBottom = '10px';
		mxUtils.write(title, titleText || mxResources.get('print'));
		div.appendChild(title);

		var currentPage = 1;
		var pageCount = 1;

		// Pages
		var pagesSection = document.createElement('div');
		pagesSection.className = 'geDialogSection';
		pagesSection.style.whiteSpace = 'nowrap';

		var allPagesRadio = document.createElement('input');
		allPagesRadio.style.marginRight = '8px';
		allPagesRadio.setAttribute('type', 'radio');
		allPagesRadio.setAttribute('name', 'pages-printdialog');

		var allPagesRow = document.createElement('div');
		allPagesRow.className = 'geDialogCheckRow';
		allPagesRow.appendChild(allPagesRadio);
		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('allPages'));
		mxEvent.addListener(span, 'click', function()
		{
			allPagesRadio.checked = true;
		});
		allPagesRow.appendChild(span);
		pagesSection.appendChild(allPagesRow);

		// Page range
		var pagesRadio = document.createElement('input');
		pagesRadio.style.marginRight = '8px';
		pagesRadio.setAttribute('type', 'radio');
		pagesRadio.setAttribute('name', 'pages-printdialog');

		var pagesRow = document.createElement('div');
		pagesRow.className = 'geDialogCheckRow';
		pagesRow.appendChild(pagesRadio);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('pages') + ':');
		pagesRow.appendChild(span);
		mxEvent.addListener(span, 'click', function()
		{
			pagesRadio.checked = true;
		});

		var pagesFromInput = document.createElement('input');
		pagesFromInput.style.margin = '0 4px';
		pagesFromInput.setAttribute('value', '1');
		pagesFromInput.setAttribute('type', 'number');
		pagesFromInput.setAttribute('min', '1');
		pagesFromInput.style.width = '40px';
		pagesRow.appendChild(pagesFromInput);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('to'));
		pagesRow.appendChild(span);

		var pagesToInput = pagesFromInput.cloneNode(true);
		pagesRow.appendChild(pagesToInput);

		mxEvent.addListener(pagesFromInput, 'focus', function()
		{
			pagesRadio.checked = true;
		});
		
		mxEvent.addListener(pagesToInput, 'focus', function()
		{
			pagesRadio.checked = true;
		});
		
		function validatePageRange()
		{
			pagesToInput.value = Math.max(1, Math.min(pageCount,
				Math.max(parseInt(pagesToInput.value), parseInt(pagesFromInput.value))));
			pagesFromInput.value = Math.max(1, Math.min(pageCount,
				Math.min(parseInt(pagesToInput.value), parseInt(pagesFromInput.value))));
		};
		
		mxEvent.addListener(pagesFromInput, 'change', validatePageRange);
		mxEvent.addListener(pagesToInput, 'change', validatePageRange);
		
		if (editorUi.pages != null)
		{
			pageCount = editorUi.pages.length;

			if (editorUi.currentPage != null)
			{
				for (var i = 0; i < pageCount; i++)
				{
					if (editorUi.currentPage == editorUi.pages[i])
					{
						currentPage = i + 1;
						break;
					}
				}
			}
		}
		
		if (editorUi.lastPrintPagesFromInput != null &&
			editorUi.lastPrintPagesToInput != null)
		{
			pagesFromInput.value = Math.min(pageCount,
				Math.max(1, editorUi.lastPrintPagesFromInput));
			pagesToInput.value = Math.min(pageCount,
				Math.max(1, editorUi.lastPrintPagesToInput));
		}
		else
		{
			pagesFromInput.value = currentPage;
			pagesToInput.value = currentPage;
		}
		
		pagesFromInput.setAttribute('max', pageCount);
		pagesToInput.setAttribute('max', pageCount);

		var currPage = mxUtils.button(mxResources.get('currentPage'), function(evt)
		{
			pagesFromInput.value = currentPage;
			pagesToInput.value = currentPage;
			pagesRadio.checked = true;
		});

		currPage.setAttribute('title', mxResources.get('currentPage'));
		currPage.style.marginLeft = '4px';
		currPage.style.maxWidth = '100px';
		currPage.style.overflow = 'hidden';
		currPage.style.textOverflow = 'ellipsis';
		currPage.style.whiteSpace = 'nowrap';

		pagesRow.appendChild(currPage);
		pagesSection.appendChild(pagesRow);
		
		if (pageCount > 1)
		{
			div.appendChild(pagesSection);
		}

		// Selection only
		var selectionSection = document.createElement('div');
		selectionSection.className = 'geDialogCheckRow';

		var selectionOnlyRadio = document.createElement('input');
		selectionOnlyRadio.setAttribute('name', 'pages-printdialog');
		selectionOnlyRadio.setAttribute('type', (pageCount == 1) ? 'checkbox' : 'radio');
		selectionOnlyRadio.style.marginRight = '8px';

		if (graph.isSelectionEmpty())
		{
			selectionOnlyRadio.setAttribute('disabled', 'disabled');
		}

		if (graph.isEnabled())
		{
			selectionSection.appendChild(selectionOnlyRadio);

			var span = document.createElement('span');
			mxUtils.write(span, mxResources.get('selectionOnly'));
			selectionSection.appendChild(span);
		}

		if (graph.isEnabled())
		{
			if (pageCount > 1)
			{
				pagesSection.appendChild(selectionSection);
			}
			else
			{
				var selectionWrapper = document.createElement('div');
				selectionWrapper.className = 'geDialogSection';
				selectionWrapper.appendChild(selectionSection);
				div.appendChild(selectionWrapper);
			}
		}

		if (!editorUi.isPagesEnabled() || editorUi.lastPrintPagesRadioChecked)
		{
			pagesRadio.checked = true;
		}
		else if (!graph.isSelectionEmpty() && editorUi.lastPrintSelectionOnlyChecked)
		{
			selectionOnlyRadio.checked = true;
		}
		else
		{
			allPagesRadio.checked = true;
		}

		if (!graph.isSelectionEmpty())
		{
			mxEvent.addListener(span, 'click', function()
			{
				selectionOnlyRadio.checked = !selectionOnlyRadio.checked;
			});
		}
		
		// --- Size section ---
		var sizeSection = document.createElement('div');
		sizeSection.className = 'geDialogSection';

		// Page view
		var pageViewSection = document.createElement('div');
		pageViewSection.className = 'geDialogCheckRow';

		var pageViewRadio = document.createElement('input');
		pageViewRadio.style.marginRight = '8px';
		pageViewRadio.setAttribute('type', 'radio');
		pageViewRadio.setAttribute('name', 'printSize');
		pageViewSection.appendChild(pageViewRadio);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('pageView'));
		pageViewSection.appendChild(span);
		mxEvent.addListener(pageViewSection, 'click', function()
		{
			pageViewRadio.checked = true;
		});

		sizeSection.appendChild(pageViewSection);

		// Crop
		var cropSection = document.createElement('div');
		cropSection.className = 'geDialogCheckRow';

		var cropRadio = document.createElement('input');
		cropRadio.style.marginRight = '8px';
		cropRadio.setAttribute('type', 'radio');
		cropRadio.setAttribute('name', 'printSize');
		cropSection.appendChild(cropRadio);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('crop'));
		cropSection.appendChild(span);
		mxEvent.addListener(cropSection, 'click', function()
		{
			cropRadio.checked = true;
		});

		sizeSection.appendChild(cropSection);

		// Fit to ...
		var fitSection = document.createElement('div');
		fitSection.className = 'geDialogCheckRow';
		fitSection.style.alignItems = 'flex-start';

		var fitRadio = document.createElement('input');
		fitRadio.style.marginTop = '4px';
		fitRadio.setAttribute('type', 'radio');
		fitRadio.setAttribute('name', 'printSize');
		fitSection.appendChild(fitRadio);
		
		var table = document.createElement('table');
		table.style.display = 'inline-block';
		table.style.borderSpacing = '0';
		var tbody = document.createElement('tbody');
		
		var row1 = document.createElement('tr');
		var row2 = row1.cloneNode(true);
		
		var td1 = document.createElement('td');
		var td2 = td1.cloneNode(true);
		var td3 = td1.cloneNode(true);
		
		var td4 = td1.cloneNode(true);
		var td5 = td1.cloneNode(true);
		var td6 = td1.cloneNode(true);
		
		td1.style.textAlign = 'right';
		td4.style.textAlign = 'right';

		mxUtils.write(td1, mxResources.get('fitTo'));
		mxEvent.addListener(fitSection, 'click', function(e)
		{
			if (mxEvent.getSource(e) != fitRadio)
			{
				fitRadio.checked = !fitRadio.checked;
			}
		});
		
		var sheetsAcrossInput = document.createElement('input');
		sheetsAcrossInput.style.margin = '0 2px';
		sheetsAcrossInput.style.boxSizing = 'border-box';
		sheetsAcrossInput.setAttribute('value', editorUi.lastPrintSheetsAcross || '1');
		sheetsAcrossInput.setAttribute('min', '1');
		sheetsAcrossInput.setAttribute('type', 'number');
		sheetsAcrossInput.style.width = '40px';
		td2.appendChild(sheetsAcrossInput);
		
		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('fitToSheetsAcross'));
		td3.appendChild(span);

		mxUtils.write(td4, mxResources.get('fitToBy'));
		
		var sheetsDownInput = sheetsAcrossInput.cloneNode(true);
		sheetsDownInput.setAttribute('value', editorUi.lastPrintSheetsDown || '1');
		td5.appendChild(sheetsDownInput);
		
		mxEvent.addListener(sheetsAcrossInput, 'click', function(e)
		{
			fitRadio.checked = true;
			mxEvent.consume(e);
		});

		mxEvent.addListener(sheetsDownInput, 'click', function(e)
		{
			fitRadio.checked = true;
			mxEvent.consume(e);
		});

		if (editorUi.lastPrintCropRadioChecked)
		{
			cropRadio.checked = true;
		}
		else if (editorUi.lastPrintFitRadioChecked)
		{
			fitRadio.checked = true;
		}
		else
		{
			pageViewRadio.checked = true;
		}

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('fitToSheetsDown'));
		td6.appendChild(span);
		
		row1.appendChild(td1);
		row1.appendChild(td2);
		row1.appendChild(td3);
		
		row2.appendChild(td4);
		row2.appendChild(td5);
		row2.appendChild(td6);
		
		tbody.appendChild(row1);
		tbody.appendChild(row2);
		table.appendChild(tbody);
		fitSection.appendChild(table);
		
		sizeSection.appendChild(fitSection);
		div.appendChild(sizeSection);

		// Border and zoom
		var optionsSection = document.createElement('div');
		optionsSection.className = 'geDialogSection';

		var borderZoomRow = document.createElement('div');
		borderZoomRow.className = 'geDialogFormRow';

		var borderLabel = document.createElement('label');
		borderLabel.className = 'geDialogFormLabel';
		mxUtils.write(borderLabel, mxResources.get('borderWidth'));
		borderZoomRow.appendChild(borderLabel);

		var borderInput = document.createElement('input');
		borderInput.setAttribute('type', 'number');
		borderInput.setAttribute('min', '0');
		borderInput.style.width = '40px';
		borderInput.value = (editorUi.lastPrintBorder != null) ?
			editorUi.lastPrintBorder : mxPrintPreview.prototype.pageMargin;
		borderZoomRow.appendChild(borderInput);

		var zoomLabel = document.createElement('label');
		zoomLabel.style.marginLeft = '12px';
		zoomLabel.style.marginRight = '4px';
		mxUtils.write(zoomLabel, mxResources.get('zoom'));
		borderZoomRow.appendChild(zoomLabel);

		var zoomInput = document.createElement('input');
		zoomInput.style.width = '60px';
		zoomInput.value = (editorUi.lastPrintZoom != null) ?
			editorUi.lastPrintZoom : '100%';
		borderZoomRow.appendChild(zoomInput);

		optionsSection.appendChild(borderZoomRow);

		// Grid
		var gridRow = document.createElement('div');
		gridRow.className = 'geDialogCheckRow';

		var gridInput = document.createElement('input');
		gridInput.setAttribute('type', 'checkbox');
		gridInput.style.marginRight = '8px';
		gridInput.checked = (editorUi.lastPrintGrid != null) ?
			editorUi.lastPrintGrid : false;
		gridRow.appendChild(gridInput);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('grid'));
		gridRow.appendChild(span);

		mxEvent.addListener(span, 'click', function(e)
		{
			gridInput.checked = true;
			mxEvent.consume(e);
		});

		optionsSection.appendChild(gridRow);

		// Shadows enabled
		var shadowsRow = document.createElement('div');
		shadowsRow.className = 'geDialogCheckRow';

		var shadowsInput = document.createElement('input');
		shadowsInput.setAttribute('type', 'checkbox');
		shadowsInput.style.marginRight = '8px';
		shadowsInput.checked = (editorUi.lastPrintShadow != null) ?
			editorUi.lastPrintShadow : false;
		shadowsRow.appendChild(shadowsInput);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('shadows'));
		shadowsRow.appendChild(span);

		if (!editorUi.isOffline() || mxClient.IS_CHROMEAPP)
		{
			span.appendChild(editorUi.createHelpIcon(
				'https://github.com/jgraph/drawio/discussions/5136'));
		}

		mxEvent.addListener(span, 'click', function(e)
		{
			if (mxEvent.getSource(e).nodeName != 'IMG')
			{
				shadowsInput.checked = true;
				mxEvent.consume(e);
			}
		});

		// Hides shadows option if not supported
		if (!Editor.enableShadowOption)
		{
			shadowsRow.style.display = 'none';
		}

		optionsSection.appendChild(shadowsRow);

		// Transparent background
		var transparentInput = document.createElement('input');
		transparentInput.setAttribute('type', 'checkbox');
		transparentInput.style.marginRight = '8px';
		transparentInput.checked = (editorUi.lastPrintTransparent != null) ?
			editorUi.lastPrintTransparent : false;

		// Export
		if (fn != null)
		{
			var transparentRow = document.createElement('div');
			transparentRow.className = 'geDialogCheckRow';
			transparentRow.appendChild(transparentInput);

			var span = document.createElement('span');
			mxUtils.write(span, mxResources.get('transparentBackground'));
			transparentRow.appendChild(span);

			mxEvent.addListener(span, 'click', function(e)
			{
				transparentInput.checked = true;
				mxEvent.consume(e);
			});

			optionsSection.appendChild(transparentRow);
		}

		// Include diagram
		var includeInput = document.createElement('input');
		includeInput.setAttribute('type', 'checkbox');
		includeInput.style.marginRight = '8px';
		includeInput.checked = (editorUi.lastPrintInclude != null) ?
			editorUi.lastPrintInclude : Editor.defaultIncludeDiagram;

		if (fn != null && !mxClient.IS_CHROMEAPP &&
			editorUi.getServiceName() == 'draw.io')
		{
			var includeRow = document.createElement('div');
			includeRow.className = 'geDialogCheckRow';
			includeRow.appendChild(includeInput);

			var span = document.createElement('span');
			mxUtils.write(span, mxResources.get('includeCopyOfMyDiagram'));
			includeRow.appendChild(span);

			mxEvent.addListener(span, 'click', function(e)
			{
				includeInput.checked = true;
				mxEvent.consume(e);
			});

			optionsSection.appendChild(includeRow);
		}


		div.appendChild(optionsSection);

		// Buttons
		var buttons = document.createElement('div');
		buttons.style.marginTop = '16px';
		buttons.style.textAlign = 'right';
		buttons.style.whiteSpace = 'nowrap';
		
		// Preview or print
		function preview(print)
		{
			editorUi.lastPrintPagesRadioChecked = pagesRadio.checked;
			editorUi.lastPrintSelectionOnlyChecked = selectionOnlyRadio.checked;
			editorUi.lastPrintCropRadioChecked = cropRadio.checked;
			editorUi.lastPrintFitRadioChecked = fitRadio.checked;

			if (pagesRadio.checked && pagesFromInput.value < pagesToInput.value)
			{
				editorUi.lastPrintPagesFromInput = pagesFromInput.value;
				editorUi.lastPrintPagesToInput = pagesToInput.value;
			}
			else
			{
				editorUi.lastPrintPagesFromInput = null;
				editorUi.lastPrintPagesToInput = null;
			}

			zoomInput.value = Math.max(1, Math.min(1600, parseInt(zoomInput.value))) + '%';
			editorUi.lastPrintZoom = zoomInput.value;
			editorUi.lastPrintBorder = borderInput.value;
			editorUi.lastPrintGrid = gridInput.checked;
			editorUi.lastPrintTransparent = transparentInput.checked;
			editorUi.lastPrintInclude = includeInput.checked;
			editorUi.lastPrintSheetsAcross = sheetsAcrossInput.value;
			editorUi.lastPrintSheetsDown = sheetsDownInput.value;

			var args = {};
			args.border = parseInt(borderInput.value);
			args.scale = parseInt(zoomInput.value) / 100;
			args.grid = gridInput.checked;
			args.shadows = shadowsInput.checked;
			args.transparent = transparentInput.checked;
			args.includeCopy = includeInput.checked;
			args.sheetsAcross = parseInt(sheetsAcrossInput.value);
			args.sheetsDown = parseInt(sheetsDownInput.value);
			args.pagesFrom = parseInt(pagesFromInput.value);
			args.pagesTo = parseInt(pagesToInput.value);
			args.allPages = allPagesRadio.checked;
			args.selection = selectionOnlyRadio.checked;
			args.pageView = pageViewRadio.checked;
			args.crop = cropRadio.checked;
			args.fit = fitRadio.checked;

			return (fn != null) ? fn(!print, args) : editorUi.print(!print, args);
		};

		if ((!editorUi.isOffline() || mxClient.IS_CHROMEAPP) && fn == null)
		{
			buttons.appendChild(editorUi.createHelpIcon(
				'https://www.drawio.com/doc/faq/print-diagram'));
		}
		
		var cancelBtn = mxUtils.button(mxResources.get('cancel'), function()
		{
			editorUi.hideDialog();
		});
		
		cancelBtn.className = 'geBtn';
		
		if (editorUi.editor.cancelFirst)
		{
			buttons.appendChild(cancelBtn);
		}
		
		if (PrintDialog.previewEnabled && fn == null)
		{
			var previewBtn = mxUtils.button(mxResources.get('preview'), function()
			{
				try
				{
					editorUi.hideDialog();
					preview(false);
				}
				catch (e)
				{
					editorUi.handleError(e);
				}
			});

			previewBtn.className = 'geBtn';
			buttons.appendChild(previewBtn);
		}

		btnTitle = (btnTitle != null) ? btnTitle : (fn != null) ? 'export' :
			(!PrintDialog.previewEnabled) ? 'ok' : 'print';
		var printBtn = mxUtils.button(mxResources.get(btnTitle), function()
		{
			try
			{
				editorUi.hideDialog();
				preview(true);
			}
			catch (e)
			{
				editorUi.handleError(e);
			}
		});

		printBtn.className = 'geBtn gePrimaryBtn';
		buttons.appendChild(printBtn);
		
		if (!editorUi.editor.cancelFirst)
		{
			buttons.appendChild(cancelBtn);
		}

		div.appendChild(buttons);

		// Handles dark mode for standalone print dialog in lightbox
		if (editorUi.editor.isChromelessView())
		{
			function updateDarkMode()
			{
				if (Editor.isDarkMode())
				{
					div.classList.add('geDarkMode');
					div.style.colorScheme = 'dark';
				}
				else
				{
					div.classList.remove('geDarkMode');
					div.style.colorScheme = 'light';
				}
			};

			editorUi.addListener('darkModeChanged', updateDarkMode);
			updateDarkMode();

			// Removes listeners after dialog was closed
			var removeListener = function(sender, evt)
			{
				var dialog = evt.getProperty('dialog');

				if (div.parentNode == dialog.container)
				{
					editorUi.editor.removeListener(removeListener);
					editorUi.removeListener(updateDarkMode);
				}
			};

			editorUi.editor.addListener('hideDialog', removeListener);
		}

		this.container = div;
	};
	
    // Execute fit page on page setup changes
    var changePageSetupExecute = ChangePageSetup.prototype.execute;
    
    ChangePageSetup.prototype.execute = function()
    {
        if (this.page == null)
        {
            this.page = this.ui.currentPage;
        }

        // Workaround for redo existing change with different current page
        if (this.page != this.ui.currentPage)
        {
            if (this.page.viewState != null)
            {
                if (!this.ignoreColor)
                {
                    this.page.viewState.background = this.color;
                }
                
                if (!this.ignoreImage)
                {
					var img = this.image;

					if (img != null && img.src != null && Graph.isPageLink(img.src))
					{
						img = {originalSrc: img.src};
					}

                    this.page.viewState.backgroundImage = img;
                }

                if (this.format != null)
                {
                    this.page.viewState.pageFormat = this.format;
                }
                
                if (this.mathEnabled != null)
                {
                    this.page.viewState.mathEnabled = this.mathEnabled;
                }
                
                if (this.adaptiveColors != null)
				{
					this.page.viewState.adaptiveColors = this.adaptiveColors;
				}
				
                if (this.shadowVisible != null)
            	{
            		this.page.viewState.shadowVisible = this.shadowVisible;
            	}
            }   
        }
        else
        {
            changePageSetupExecute.apply(this, arguments);
            
            if (this.mathEnabled != null && this.mathEnabled != this.ui.isMathEnabled())
            {
                this.ui.setMathEnabled(this.mathEnabled);
                this.mathEnabled = !this.mathEnabled;
            }
			
            if (this.adaptiveColors != null && this.adaptiveColors != this.ui.editor.graph.adaptiveColors)
			{
				var temp = this.ui.editor.graph.adaptiveColors
				this.ui.setAdaptiveColors(this.adaptiveColors);
				this.adaptiveColors = (temp != null) ? temp : 'default';
			}
			
            if (this.shadowVisible != null && this.shadowVisible != this.ui.editor.graph.shadowVisible)
            {
            	this.ui.editor.graph.setShadowVisible(this.shadowVisible);
                this.shadowVisible = !this.shadowVisible;
            }
        }
    };
})();