/
githubmirror
/
drawio
Обзор
Документация
Войти
/
githubmirror
/
drawio
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
src/main/webapp/js/diagramly/Editor.js
13 564 строки
424 KB
David Benson
31.1.8 release
07 авг 2026, 02:43
07 авг 2026, 02:43
a1f615b
Код
Авторство
О чём код?
/** * 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.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.lightDarkCommentImage = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBzdHlsZT0iY29sb3Itc2NoZW1lOmxpZ2h0IGRhcms7IiBmaWxsPSJsaWdodC1kYXJrKCMwMDAwMDAsI2ZmZmZmZikiPjxwYXRoIGQ9Ik0yNDAtNDAwaDQ4MHYtODBIMjQwdjgwWm0wLTEyMGg0ODB2LTgwSDI0MHY4MFptMC0xMjBoNDgwdi04MEgyNDB2ODBaTTgwLTgwdi03MjBxMC0zMyAyMy41LTU2LjVUMTYwLTg4MGg2NDBxMzMgMCA1Ni41IDIzLjVUODgwLTgwMHY0ODBxMCAzMy0yMy41IDU2LjVUODAwLTI0MEgyNDBMODAtODBabTEyNi0yNDBoNTk0di00ODBIMTYwdjUyNWw0Ni00NVptLTQ2IDB2LTQ4MCA0ODBaIi8+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; /** * Requests data URIs from the icon search service so inserted icons * are self-contained (no remote image references in the file). Off by * default online to keep files small. */ Editor.inlineExtIcons = false; /** * 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; } /** * Language bundles installed for offline use on startup (array of * language codes, see the defaultLanguages configuration key). The * service worker validates each code against its lazy manifest. */ Editor.defaultLanguages = null; /** * 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; /** * If true, label autosize, view bounds and the post-load fit-to-window use * the MathJax-rendered math size rather than the raw formula text size, * and the canvas is hidden during typesetting to avoid showing the source. * Default is true. See [jgraph/drawio#3311]. */ Editor.mathOutputSize = true; /** * Specifies animations should be enabled. Default is true. */ Editor.enableAnimations = true; /** * Specifies if insert animations should be shown. Default is true. */ Editor.insertAnimations = 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 icons should be shown on cells with a note. * Default is true. */ Editor.showNoteIcons = true; /** * Specifies the tooltip font size in pixels. Default is null (uses CSS default of 11px). */ Editor.tooltipFontSize = null; /** * Specifies the tooltip max-width in pixels. Default is 360. 0 means no limit. */ Editor.tooltipMaxWidth = 360; /** * 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 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. When asked to ' + 'modify a diagram that is given in the conversation as draw.io XML, return the complete updated ' + 'diagram as draw.io XML, not MermaidJS, and keep the existing id attribute of every cell that is ' + 'kept so it can be matched to the diagram on the canvas. 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. If the prompt is a question or does not ask ' + 'for a diagram, answer it as plain text without any diagram code instead.', 'update': 'You are a helpful assistant that helps with ' + 'the following draw.io diagram and returns an updated draw.io diagram if needed. When ' + 'asked to modify the diagram, return the complete updated diagram as draw.io XML without ' + 'indentation and newlines - never as MermaidJS or any other format - and keep the existing id ' + 'attribute of every cell that is kept so it can be matched to the diagram on the canvas. 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}', system: '{action}', messages: [ {role: 'user', content: '{prompt}'} ], }, responsePath: '$.content[0].text' } }; /** * Adds a list of available AI models. */ Editor.aiModels = [ {name: 'Claude 4.8 Opus', model: 'claude-opus-4-8', config: 'claude'}, {name: 'Claude 4.6 Opus', model: 'claude-opus-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'}, ]; /** * Maximum number of characters (prompt, attached context and conversation * history combined) sent to the hosted AI service, which is free for the * user and paid for by draw.io. The conversation is truncated to the * newest turns that fit; a prompt/attachment that alone exceeds the limit * is rejected with the promptTooLarge error. 0 disables the limit. * BYO-key models are not limited. */ Editor.maxPublicPromptLength = 10000; /** * The Atlassian deployments (ac.draw.io / aj.draw.io Connect apps and the * Forge CDN) get a larger prompt budget, configured via the * DRAWIO_ATLASSIAN_PROMPT_LENGTH global (Init.js). The value must not * exceed the per-origin limit enforced by the generate/v3 worker * (MAX_PROMPT_LENGTH_ATLASSIAN in cf-workers/generate3/lib/security.js). */ if (window.location.hostname == 'ac.draw.io' || window.location.hostname == 'aj.draw.io' || /\.cdn\.prod\.atlassian-dev\.net$/.test(window.location.hostname) || /\.cdn\.prod\.atlassian-dev-us-gov-mod\.net$/.test(window.location.hostname)) { Editor.maxPublicPromptLength = window.DRAWIO_ATLASSIAN_PROMPT_LENGTH; } /** * 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 if fonts should be embedded as data URIs in exported and saved * SVG files. If disabled, external font references are added instead. * Default is true. */ Editor.embedSvgFonts = 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); } } }; }; /** * Returns true if the given style defines a pattern fill style that is * rendered as an SVG fill pattern when sketch mode is disabled. */ Editor.isPatternFillStyle = function(style) { var fillStyle = mxUtils.getValue(style, 'fillStyle', 'auto'); return fillStyle != 'auto' && fillStyle != 'solid'; }; /** * 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' || Editor.isPatternFillStyle(state.style)) && 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' || Editor.isPatternFillStyle(state.style)) && state.vertices.length > 0; }}, {name: 'hachureAngle', dispName: 'Hachure Angle', type: 'int', defVal: -41, isVisible: function(state, format) { // The dots pattern ignores the hachure angle in normal mode return (mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' || (Editor.isPatternFillStyle(state.style) && mxUtils.getValue(state.style, 'fillStyle', 'auto') != 'dots')) && 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'}, {val: 'northsouth', dispName: 'North + South'}, {val: 'eastwest', dispName: 'East + 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'}, {val: 'northsouth', dispName: 'North + South'}, {val: 'eastwest', dispName: 'East + 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: 'shapeInsideShape', dispName: 'Text Flow Shape', type: 'enum', defVal: null, enumList: [{val: null, dispName: 'Automatic'}, {val: 'dataStorage', dispName: 'Data Storage'}, {val: 'ellipse', dispName: 'Ellipse'}, {val: 'hexagon', dispName: 'Hexagon'}, {val: 'or', dispName: 'Or'}, {val: 'parallelogram', dispName: 'Parallelogram'}, {val: 'rhombus', dispName: 'Rhombus'}, {val: 'step', dispName: 'Step'}, {val: 'trapezoid', dispName: 'Trapezoid'}, {val: 'triangle', dispName: 'Triangle'}, {val: 'xor', dispName: 'Xor'}], onChange: function(graph, newValue) { // Enables the text flow with an explicit flow shape so // that the effect is immediately visible if (newValue != null && newValue != '') { var cells = graph.getSelectionCells(); var vertices = []; for (var i = 0; i < cells.length; i++) { if (graph.model.isVertex(cells[i])) { vertices.push(cells[i]); } } if (vertices.length > 0) { graph.setCellStyles('shapeInside', '1', vertices); } } }}, {name: 'shapeInsidePadding', dispName: 'Text Flow Padding', type: 'float', min: 0, defVal: 2}, {name: 'noLabel', dispName: 'Hide Label', type: 'bool', defVal: false}, {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'}, {val: 'northsouth', dispName: 'North + South'}, {val: 'eastwest', dispName: 'East + 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: 'outlineConnect', dispName: 'Outline Connect', defVal: '1', type: 'enum', enumList: [{val: '1', dispName: 'Default'}, {val: '0', dispName: 'Delayed'}, {val: '2', dispName: 'Always'}, {val: '3', dispName: 'Never'}] }, {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: 'containerType', dispName: 'Container Type', type: 'enum', defVal: null, enumList: [{val: null, dispName: 'Default'}, {val: 'tree', dispName: 'Tree'}], isVisible: 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.isContainer(cell) || graph.model.getChildCount(cell) > 0 || state.style['containerType'] != null); }}, {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: 'foldingIconSize', dispName: 'Folding Icon Size', type: 'int', min: 1, getDefaultValue: function(state, format) { return format.editorUi.editor.graph.expandedImage.width; }, isVisible: function(state, format) { var graph = format.editorUi.editor.graph; return state.vertices.length == 1 && state.edges.length == 0 && graph.isCellFoldable(state.vertices[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: 'selectParentFirst', dispName: 'Select Parent First', type: 'bool', defVal: false, isVisible: function(state, format) { if (state.vertices.length != 1 || state.edges.length > 0) { return false; } var cell = state.vertices[0]; var graph = format.editorUi.editor.graph; return graph.isSwimlane(cell) || graph.model.getChildCount(cell) > 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: 'editIcon', dispName: 'Edit Icon', type: 'bool', defVal: false}, {name: 'moveIcon', dispName: 'Move Icon', type: 'bool', defVal: false, 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.isTransparentBounds != null && graph.isTransparentBounds(cell); }}, {name: 'connectIcon', dispName: 'Connect Icon', type: 'bool', getDefaultValue: function(state, format) { return Editor.showConnectHandle === true; }}, {name: 'lockedGroup', dispName: 'Locked Group', type: 'enum', defVal: null, enumList: [{val: null, dispName: 'Default'}, {val: '0', dispName: 'Unlocked'}, {val: '1', dispName: 'Locked'}], isVisible: function(state, format) { return state.vertices.length > 0 && state.edges.length == 0; } }, {name: 'lockedGroupIcon', dispName: 'Lock Icon', type: 'enum', defVal: null, enumList: [{val: null, dispName: 'Default'}, {val: '0', dispName: 'Disabled'}, {val: '1', dispName: 'Visible'}], isVisible: function(state, format) { return state.vertices.length > 0 && state.edges.length == 0; } }, {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, the ELK shorthands\n' + '## elkRadial, elkOrganic, elkStress, libavoid (orthogonal edge routing), parallels\n' + '## (spread overlapping parallel edges), or a JSON string as used in Layout, Apply.\n' + '## 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); var prev = (ctx.root != null) ? ctx.root.lastChild : null; this._drawToContext(ctx, drawing, o); // Tags the hachure fill path so getFlowAnimationPath skips it: // it is stroked in the FILL color and would otherwise win the // flow animation path selection for filled sketch shapes. if (ctx.root != null && ctx.root.lastChild != null && ctx.root.lastChild != prev && ctx.root.lastChild.nodeName == 'path') { ctx.root.lastChild.setAttribute('data-rough-fill', '1'); } 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') { // Resolves light-dark colors to the currently rendered value before the // background comparison. Otherwise hex2rgb/color2hex resolves them to the // light value (detached canvas uses color-scheme normal), so any light-dark // fill with a white light value renders solid even in dark mode (where the // dark value applies and differs from the background). var preferDark = mxUtils.lightDarkColorSupported ? Editor.isDarkMode() : mxUtils.preferDarkColor; var resolveColor = function(color) { if (mxUtils.isLightDarkColor(color)) { var ld = mxUtils.parseLightDarkColor(color); color = preferDark ? ld.dark : ld.light; } return mxUtils.hex2rgb(color); }; // One of the following backgrounds for solid fill var bg = [resolveColor('#ffffff')]; if (this.shape.state != null) { bg.push(resolveColor(this.shape.state.view.graph.shapeBackgroundColor)); } if (Editor.isDarkMode()) { bg.push(resolveColor(Editor.darkColor)); } fillStyle = (style.fill != null && (gradient != null || mxUtils.indexOf( bg, resolveColor(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 inline or referenced subject from other PDF versions, // including files rewritten by external editors (eg. Preview // saves PDF 1.3 with the subject as a literal string) if (result == null && f.substring(0, 5) == '%PDF-') { 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) { // Decodes the string escapes of external editors that rewrite // the subject as a literal string (eg. Preview escapes the // backslashes of the embedded subject), then the escaped // parentheses of the embedded subject result = decodeURIComponent(result. replace(/\\([\\()])/g, '$1'). 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; } // Keyboard chords the embedding host wants to handle itself (e.g. // Ctrl+P for VS Code Quick Open). In embed mode draw.io intercepts // these in the capture phase, suppresses its own handling, and posts // the host's command back, so a host can reclaim shortcuts even across // a cross-origin iframe where it cannot inject its own key listener. // Each entry is {key, ctrl, shift, alt, command}; see // EditorUi.installPassThroughKeys. if (config.passThroughKeys != null) { Editor.passThroughKeys = config.passThroughKeys; } // Custom keyboard shortcuts that bind keys to actions and // override the default bindings. Each entry is {keyCode, // control, shift, alt, action} where action is the name of // an entry in EditorUi.actions, or null to remove an // existing binding. See EditorUi.installKeyboardShortcuts. if (config.keyboardShortcuts != null) { if (Array.isArray(config.keyboardShortcuts)) { Editor.keyboardShortcuts = config.keyboardShortcuts; } else { EditorUi.debug('Configuration Error: Array expected for keyboardShortcuts'); } } // When set, the host blocks new windows/tabs (e.g. a VS Code webview): // link clicks are forwarded to the host via {event:'openLink'} instead // of window.open, and "open in new window/tab" options are hidden. // Implies popupsAllowed=false. See Editor.suppressNewWindows. if (config.suppressNewWindows) { Editor.suppressNewWindows = true; Editor.popupsAllowed = false; } 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); } // Overrides existing or adds new language resources for // mxResources.get. Maps from resource keys to strings, or to // objects with one entry per language and main as the fallback, // eg. {"saveAs": {"main": "Save a Copy", "de": "Kopie speichern"}, // "myKey": "My Text"}. See Editor.applyCustomResources. if (config.resources != null) { if (typeof config.resources === 'object' && !Array.isArray(config.resources)) { Editor.customResources = config.resources; Editor.applyCustomResources(); } else { EditorUi.debug('Configuration Error: Object expected for resources'); } } if (config.enableLocalFonts != null) { Editor.enableLocalFonts = config.enableLocalFonts; } if (config.enableCustomGitLabUrl != null) { Editor.enableCustomGitLabUrl = config.enableCustomGitLabUrl; } 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.swimlaneSelectionEnabled != null) { Graph.prototype.swimlaneSelectionEnabled = config.swimlaneSelectionEnabled; } if (config.autosaveDelay != null) { DrawioFile.prototype.autosaveDelay = config.autosaveDelay } if (config.templateFile != null) { EditorUi.templateFile = config.templateFile; } if (config.enabledTemplateSections != null) { if (Array.isArray(config.enabledTemplateSections)) { EditorUi.enabledTemplateSections = config.enabledTemplateSections; } else { EditorUi.debug('Configuration Error: Array expected for enabledTemplateSections'); } } if (config.customTemplates != null) { if (Array.isArray(config.customTemplates)) { EditorUi.customTemplates = config.customTemplates; } else { EditorUi.debug('Configuration Error: Array expected for customTemplates'); } } 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.compressStyles != null) { var modelCodec = mxCodecRegistry.getCodec(mxGraphModel); if (modelCodec != null) { modelCodec.enableStyleCompression = config.compressStyles; } } 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.mathOutputSize != null) { Editor.mathOutputSize = config.mathOutputSize; } // Default Mermaid config used when inserting/editing Mermaid // diagrams (theme, themeVariables, per-diagram-type options, ...), // letting a deployment enforce a unified Mermaid look. The // security-critical keys (securityLevel, startOnLoad, maxTextSize) // are re-forced in EditorUi.getMermaidConfig after this default is // cloned, so they cannot be weakened here. if (config.mermaid != null) { if (typeof config.mermaid === 'object' && !Array.isArray(config.mermaid)) { EditorUi.defaultMermaidConfig = config.mermaid; } else { EditorUi.debug('Configuration Error: Object expected for mermaid'); } } 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.showNoteIcons != null) { Editor.showNoteIcons = config.showNoteIcons; } if (config.tooltipFontSize != null) { var val = parseInt(config.tooltipFontSize); if (!isNaN(val) && val > 0) { Editor.tooltipFontSize = val; } else { EditorUi.debug('Configuration Error: Int > 0 expected for tooltipFontSize'); } } if (config.tooltipMaxWidth != null) { var val = parseInt(config.tooltipMaxWidth); if (!isNaN(val) && val >= 0) { Editor.tooltipMaxWidth = val; } else { EditorUi.debug('Configuration Error: Int >= 0 expected for tooltipMaxWidth'); } } if (config.expandPatternsForPrint != null) { Editor.expandPatternsForPrint = config.expandPatternsForPrint; } if (config.showConnectHandle != null) { Editor.showConnectHandle = config.showConnectHandle; } if (config.enableInlineToolbar != null) { Editor.enableInlineToolbar = config.enableInlineToolbar; } // Handlers are not defined in the embedded graph if (config.enableSizeGuides != null && typeof mxVertexHandler !== 'undefined') { mxVertexHandler.prototype.sizeGuidesEnabled = config.enableSizeGuides; } if (config.enablePositionGuides != null && typeof mxGuide !== 'undefined') { mxGuide.prototype.positionEnabled = config.enablePositionGuides; } if (config.enableDistanceGuides != null && typeof mxGuide !== 'undefined') { mxGuide.prototype.distanceEnabled = config.enableDistanceGuides; } if (config.defaultTransparentGroups != null) { Editor.defaultTransparentGroups = config.defaultTransparentGroups; } 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; } if (config.defaultPageBackgroundColor != null) { Editor.pageBackgroundColor = config.defaultPageBackgroundColor; } if (config.defaultDarkPageBackgroundColor != null) { Editor.darkPageBackgroundColor = config.defaultDarkPageBackgroundColor; } if (config.defaultGridColor != null) { mxGraphView.prototype.defaultGridColor = config.defaultGridColor; } if (config.defaultDarkGridColor != null) { mxGraphView.prototype.defaultDarkGridColor = config.defaultDarkGridColor; } // Updates the initial grid color for the current theme mxGraphView.prototype.gridColor = (Editor.isDarkMode()) ? mxGraphView.prototype.defaultDarkGridColor : mxGraphView.prototype.defaultGridColor; // Updates colors that depend on Editor.darkColor // LATER: Add event to update darkColor dependencies Graph.prototype.defaultPageBackgroundColor = Editor.getDefaultPageBackgroundColor(); 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; } // Language bundles installed for offline use on startup if (config.defaultLanguages != null) { if (Array.isArray(config.defaultLanguages)) { Editor.defaultLanguages = config.defaultLanguages; } else { EditorUi.debug('Configuration Error: Array expected for defaultLanguages'); } } // Disables custom libraries if (config.enableCustomLibraries != null) { Editor.enableCustomLibraries = config.enableCustomLibraries; } // Inlines icon search results as data URIs on insert if (config.inlineExtIcons != null) { Editor.inlineExtIcons = config.inlineExtIcons; } // 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 copy on connect if (config.copyOnConnect != null) { mxConnectionHandler.prototype.createTarget = config.copyOnConnect; } // Overrides stop editing on enter if (config.stopEditingOnEnter != null) { mxGraph.prototype.enterStopsCellEditing = config.stopEditingOnEnter; } // Overrides default folding enabled if (config.defaultFoldingEnabled != null) { Graph.prototype.defaultFoldingEnabled = config.defaultFoldingEnabled; } // Overrides folding icon size if (config.foldingIconSize != null) { Graph.updateFoldingImages(config.foldingIconSize); } // Overrides mouse wheel function if (config.zoomWheel != null) { Graph.zoomWheel = config.zoomWheel; } // Enables browser translation mirror if (config.browserTranslate != null) { Graph.browserTranslate = config.browserTranslate; } // 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.embedSvgFonts != null) { Editor.embedSvgFonts = config.embedSvgFonts; } 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.insertAnimations != null) { Editor.insertAnimations = config.insertAnimations; } 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; } } }; /** * Applies Editor.customResources to mxResources, see Editor.configure. * Values with one entry per language are resolved using the lowercase * current language, the part before the dash (eg. de for de-ch) or * main, in this order. */ Editor.applyCustomResources = function() { if (Editor.customResources != null) { var lan = (mxClient.language != null) ? mxClient.language.toLowerCase() : null; var dash = (lan != null) ? lan.indexOf('-') : -1; for (var key in Editor.customResources) { var value = Editor.customResources[key]; if (value != null && typeof value === 'object') { value = ((lan != null) ? value[lan] : null) || ((dash > 0) ? value[lan.substring(0, dash)] : null) || value['main']; } if (typeof value === 'string') { mxResources.resources[key] = value; } } } }; // Reapplies custom resources after each parsed resource bundle so that // configured values take precedence over the built-in language files, // including after the language was changed var mxResourcesParse = mxResources.parse; mxResources.parse = function(text) { mxResourcesParse.apply(this, arguments); Editor.applyCustomResources(); }; /** * */ 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; // Per-call prefix so multi-page print output (multiple SVGs in one // document) can't collide on clipPath ids — without it the second // SVG's url(#expand-pattern-clip-1) resolves to the first SVG's clip // and the pattern fill is clipped off the page. var clipPrefix = 'expand-pattern-clip-' + Editor.guid(8) + '-'; /** * 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}; }; /** * 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')); // Bbox is already in the element's user coordinate system, which is // the space the pattern's userSpaceOnUse references. Do not apply the // element's CTM — that would shift coordinates into SVG viewport space // and miscompute the tile range whenever an ancestor group has a // transform (notably the print preview's CSS-transform scale+translate // when crop or fit is enabled). 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} ]; // 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 = clipPrefix + (++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 : '') + ')'); // Replicates the implicit clipping of pattern content to the tile // (overflow is hidden on SVG patterns) so that expanded tiles // render exactly like the on-screen pattern, eg. the default // hatch stroke is centered on the tile edge and only half of // its width is visible var tileClipId = clipPrefix + (++clipCounter); var tileClip = svgRoot.ownerDocument.createElementNS(svgNS, 'clipPath'); tileClip.setAttribute('id', tileClipId); var tileRect = svgRoot.ownerDocument.createElementNS(svgNS, 'rect'); tileRect.setAttribute('x', '0'); tileRect.setAttribute('y', '0'); tileRect.setAttribute('width', pw); tileRect.setAttribute('height', ph); tileClip.appendChild(tileRect); // 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) + ')'); // Evaluated in the tile's user space so the shared // rect clips each tile in its local coordinates tileGroup.setAttribute('clip-path', 'url(#' + tileClipId + ')'); 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); defs.appendChild(tileClip); // 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 = 90000; /** * 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; Editor.mathJaxRendering = false; function mathJaxDone() { rendering = null; Editor.mathJaxRendering = false; 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; Editor.mathJaxRendering = true; 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) { // Hides container until typeset completes to avoid // showing the raw formula source. For a synchronous // typeset, hide and show happen in the same tick. if (Editor.mathOutputSize) { this.graph.container.style.visibility = 'hidden'; } Editor.MathJaxRender(this.graph.container); } }); this.graph.model.addListener(mxEvent.CHANGE, renderMath); this.graph.addListener(mxEvent.REFRESH, renderMath); // Refreshes cached label bounds after MathJax has typeset, so // view bounds (used for export/scrollbars) reflect the rendered // math size rather than the raw formula text size. var graph = this.graph; var prevOnMathJaxDone = Editor.onMathJaxDone; Editor.onMathJaxDone = function() { if (prevOnMathJaxDone != null) { prevOnMathJaxDone.apply(this, arguments); } if (Editor.mathOutputSize && graph != null && graph.mathEnabled) { graph.refreshMathBounds(); if (graph.container != null) { graph.container.style.visibility = ''; } } }; }; 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); } } }; /** * Returns true if the given label contains TeX or AsciiMath delimiters * that MathJax would typeset. */ Editor.containsMath = function(text) { return text != null && (text.indexOf('$') >= 0 || text.indexOf('\\(') >= 0 || text.indexOf('\\[') >= 0 || text.indexOf('\\begin{') >= 0); }; // Overrides autosize so that labels with math are measured after MathJax // has typeset them, rather than measuring the raw formula source. (function() { var graphGetPreferredSizeForCell = Graph.prototype.getPreferredSizeForCell; var measureDiv = null; Graph.prototype.getPreferredSizeForCell = function(cell, w, gridEnabled) { if (!Editor.mathOutputSize || !this.mathEnabled || typeof MathJax === 'undefined' || typeof MathJax.typeset !== 'function' || this.model.isEdge(cell)) { return graphGetPreferredSizeForCell.apply(this, arguments); } var state = this.view.createState(cell); var label = (state != null) ? this.cellRenderer.getLabelValue(state) : null; if (label == null || label.length === 0 || !Editor.containsMath(label)) { return graphGetPreferredSizeForCell.apply(this, arguments); } var origGetSizeForString = mxUtils.getSizeForString; mxUtils.getSizeForString = function(text, fontSize, fontFamily, textWidth) { if (measureDiv == null) { measureDiv = document.createElement('div'); measureDiv.style.cssText = 'position:absolute;visibility:hidden;' + 'left:-10000px;top:-10000px;display:inline-block;'; document.body.appendChild(measureDiv); } measureDiv.style.fontSize = fontSize + 'px'; measureDiv.style.fontFamily = fontFamily; if (textWidth != null && textWidth > 0) { measureDiv.style.width = textWidth + 'px'; measureDiv.style.whiteSpace = 'normal'; } else { measureDiv.style.width = ''; measureDiv.style.whiteSpace = 'nowrap'; } // Clears MathJax state attached by previous typeset MathJax.typesetClear([measureDiv]); measureDiv.innerHTML = Graph.sanitizeHtml(text); try { MathJax.typeset([measureDiv]); var rect = measureDiv.getBoundingClientRect(); return new mxRectangle(0, 0, rect.width, rect.height); } catch (e) { // Fonts may not be loaded yet; fall back to text measurement return origGetSizeForString.apply(this, arguments); } }; try { return graphGetPreferredSizeForCell.apply(this, arguments); } finally { mxUtils.getSizeForString = origGetSizeForString; } }; })(); /** * Re-reads the rendered DOM size of every label that contains math so the * view's bounding box reflects MathJax's rendered output instead of the raw * formula source. Cell geometry is not modified. No-op when nothing math-y * is in the diagram. */ Graph.prototype.refreshMathBounds = function() { if (!Editor.mathOutputSize || !this.mathEnabled) { return; } var view = this.view; var hasMath = false; view.states.visit(mxUtils.bind(this, function(key, state) { if (state == null || state.text == null) { return; } var label = this.cellRenderer.getLabelValue(state); if (typeof label === 'string' && Editor.containsMath(label)) { state.text.updateBoundingBox(); hasMath = true; } })); if (!hasMath) { return; } var rootCell = (view.currentRoot != null) ? view.currentRoot : this.model.getRoot(); var rootState = view.getState(rootCell); if (rootState != null) { var bounds = view.getBoundingBox(rootState, true, false); view.setGraphBounds((bounds != null) ? bounds : view.getEmptyBounds()); this.sizeDidChange(); } this.fireEvent(new mxEventObject('mathRefreshed')); }; // Re-runs initial fit-to-window after MathJax has typeset, so the fit uses // the rendered math bounds rather than the (still wrong) raw-text bounds // that were available at file-load time. (function() { var editorUiInitialFitDiagram = EditorUi.prototype.initialFitDiagram; EditorUi.prototype.initialFitDiagram = function(maxScale) { editorUiInitialFitDiagram.apply(this, arguments); var graph = this.editor.graph; if (!Editor.mathOutputSize || graph == null || !graph.mathEnabled) { return; } var args = arguments; var ui = this; var fired = false; // Registers the mathRefreshed listener synchronously so it captures // the typeset that follows. On page switch with fonts already loaded // MathJax typesets synchronously inside the same dispatch as // initialFitDiagram (pageSelected fires from change.execute, then // edit.notify fires CHANGE → renderMath → typeset → mathRefreshed), // so a deferred registration would miss the event. var listener = function() { if (fired) { return; } fired = true; graph.removeListener(listener); // Reset to "fresh-load" defaults (scale=1, default scroll) // so that re-running initialFitDiagram with zoomOutOnly=true // either no-ops cleanly (matching a no-math page's fresh // state) or zooms out as needed for oversized content. graph.zoomTo(1); ui.resetScrollbars(); editorUiInitialFitDiagram.apply(ui, args); }; graph.addListener('mathRefreshed', listener); // Defers a cleanup check to a microtask so the listener doesn't // leak when MathJax is unavailable or the diagram contains no math // to typeset (mathRefreshed never fires in that case). Promise.resolve().then(function() { if (fired) { return; } var pending = (typeof MathJax === 'undefined') || (typeof MathJax.typeset !== 'function') || (Editor.mathJaxQueue != null && Editor.mathJaxQueue.length > 0) || Editor.mathJaxRendering; if (!pending) { graph.removeListener(listener); } }); }; })(); /** * 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, 27) === 'https://icons.diagrams.net/' || 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 SVG images referenced in use tags, eg. for themed images // with cssVars, into local symbols with the theming per use tag var symbolCache = new Object(); var getDefs = function() { var defs = svgRoot.getElementsByTagName('defs')[0]; if (defs == null) { var doc = svgRoot.ownerDocument; defs = (doc.createElementNS != null) ? doc.createElementNS(mxConstants.NS_SVG, 'defs') : doc.createElement('defs'); svgRoot.insertBefore(defs, svgRoot.firstChild); } return defs; }; var convertUses = mxUtils.bind(this, function() { var uses = svgRoot.getElementsByTagName('use'); for (var i = 0; i < uses.length; i++) { (mxUtils.bind(this, function(use) { pending.push(mxUtils.bind(this, function() { try { var href = use.getAttribute('href'); if (href == null) { href = use.getAttribute('xlink:href'); } if (href != null && href.charAt(0) != '#' && href.substring(0, 5) != 'data:') { var src = converter.convert(href); var inlineSymbol = function(uri) { var id = Graph.createSvgImageSymbol( getDefs(), symbolCache, uri, true); if (id != null) { use.setAttribute('href', '#' + id); use.setAttribute('xlink:href', '#' + id); } next(); }; var tmp = cache[src]; if (tmp == null) { this.convertImageToDataUri(src, function(uri) { if (uri != null && uri != src) { cache[src] = uri; } inlineSymbol(uri); }); } else { inlineSymbol(tmp); } } else { next(); } } catch (e) { next(); } })); }))(uses[i]); } }); // Collapses images that share the same data so that the data is // stored only once, eg. for images with the same URL that were // embedded at multiple sizes or aspect ratios, including inline // instances. Uses symbols with the intrinsic size of the image so // that use tags can scale the shared content, with one symbol per // aspect mode that reference a shared image for the data. Images // that match the source of an existing symbol for themed images // are redirected to that symbol. var dedupImageDefs = mxUtils.bind(this, function() { pending.push(mxUtils.bind(this, function() { try { var temp = svgRoot.getElementsByTagName('image'); var images = []; for (var i = 0; i < temp.length; i++) { images.push(temp[i]); } var redirects = {}; var payloads = {}; var keys = []; var retargetUses = function() { var uses = svgRoot.getElementsByTagName('use'); for (var i = 0; i < uses.length; i++) { var ref = uses[i].getAttribute('href'); if (ref == null) { ref = uses[i].getAttribute('xlink:href'); } if (ref != null && redirects[ref] != null) { uses[i].setAttribute('href', '#' + redirects[ref]); uses[i].setAttribute('xlink:href', '#' + redirects[ref]); } } }; var replaceWithUse = function(img, id) { var use = (svgRoot.ownerDocument.createElementNS != null) ? svgRoot.ownerDocument.createElementNS(mxConstants.NS_SVG, 'use') : svgRoot.ownerDocument.createElement('use'); // Copies position and rendering attributes var attrs = img.attributes; for (var j = 0; j < attrs.length; j++) { var name = attrs[j].name; if (name != 'href' && name != 'xlink:href' && name != 'preserveAspectRatio') { use.setAttribute(name, attrs[j].value); } } use.setAttribute('href', '#' + id); use.setAttribute('xlink:href', '#' + id); img.parentNode.replaceChild(use, img); }; for (var i = 0; i < images.length; i++) { var img = images[i]; var def = img; var inline = true; if (img.parentNode.nodeName == 'symbol') { // Accepts images wrapped in a symbol for shared // sizing and ignores other symbol content if (img.getAttribute('id') == null && String(img.parentNode.getAttribute('id') || ''). substring(0, 9) == 'mx-image-') { def = img.parentNode; inline = false; } else { continue; } } else if (String(img.getAttribute('id') || ''). substring(0, 9) == 'mx-image-') { inline = false; } var href = img.getAttribute('xlink:href'); if (href != null && href.substring(0, 5) == 'data:') { var par = def.getAttribute('preserveAspectRatio') || ''; // Redirects to an existing symbol for themed images // with the same source, which have a deterministic ID if (href.substring(0, 18) == 'data:image/svg+xml') { var symbolId = 'mx-symbol-' + mxUtils.hashCode( href + '|' + (par != 'none')); var symbol = svgRoot.querySelector('[id="' + symbolId + '"]'); if (symbol != null && symbol.nodeName == 'symbol') { if (inline) { replaceWithUse(img, symbolId); } else { redirects['#' + def.getAttribute('id')] = symbolId; def.parentNode.removeChild(def); } continue; } } if (payloads[href] == null) { payloads[href] = {}; keys.push(href); } if (payloads[href][par] == null) { payloads[href][par] = []; } payloads[href][par].push({node: def, inline: inline}); } } retargetUses(); var processNext = function() { var href = null; var work = null; // Finds next payload with multiple instances or aspects while (keys.length > 0 && href == null) { href = keys.pop(); work = payloads[href]; var count = 0; var members = 0; for (var par in work) { count++; members = Math.max(members, work[par].length); } if (count < 2 && members < 2) { href = null; } } if (href == null) { next(); } else { var loader = new Image(); loader.onload = function() { try { var w = loader.naturalWidth; var h = loader.naturalHeight; if (w > 0 && h > 0) { var doc = svgRoot.ownerDocument; var aspects = 0; for (var par in work) { aspects++; } var createElt = function(tag) { return (doc.createElementNS != null) ? doc.createElementNS(mxConstants.NS_SVG, tag) : doc.createElement(tag); }; var createImage = function() { var inner = createElt('image'); inner.setAttribute('width', w); inner.setAttribute('height', h); // Workaround for missing namespace support if (inner.setAttributeNS == null) { inner.setAttribute('xlink:href', href); } else { inner.setAttributeNS(mxConstants.NS_XLINK, 'xlink:href', href); } return inner; }; var shared = null; // Stores the data in a shared image if it is // used with multiple aspect modes if (aspects > 1) { shared = 'mx-data-' + mxUtils.hashCode(href); // Resolves collisions with existing IDs while (svgRoot.querySelector('[id="' + shared + '"]') != null) { shared += '-1'; } var data = createImage(); data.setAttribute('id', shared); getDefs().appendChild(data); } for (var par in work) { var imgs = work[par]; var id = null; // Reuses the ID of the first shared definition for (var i = 0; i < imgs.length && id == null; i++) { if (!imgs[i].inline) { id = imgs[i].node.getAttribute('id'); } } if (id == null) { id = 'mx-image-' + mxUtils.hashCode(href + '|' + par); // Resolves collisions with existing IDs while (svgRoot.querySelector('[id="' + id + '"]') != null) { id += '-1'; } } var symbol = createElt('symbol'); symbol.setAttribute('id', id); symbol.setAttribute('viewBox', '0 0 ' + w + ' ' + h); if (par != '') { symbol.setAttribute('preserveAspectRatio', par); } if (shared != null) { var use = createElt('use'); use.setAttribute('href', '#' + shared); use.setAttribute('xlink:href', '#' + shared); symbol.appendChild(use); } else { symbol.appendChild(createImage()); } getDefs().appendChild(symbol); // Replaces inline images with use tags and // removes the shared definitions for (var i = 0; i < imgs.length; i++) { var node = imgs[i].node; if (imgs[i].inline) { replaceWithUse(node, id); } else { if (node.getAttribute('id') != id) { redirects['#' + node.getAttribute('id')] = id; } node.parentNode.removeChild(node); } } } retargetUses(); } } catch (e) { // ignore } processNext(); }; loader.onerror = function() { processNext(); }; loader.src = href; } }; processNext(); } catch (e) { next(); } })); }); // Converts all known image tags in output // LATER: Add support for images in CSS convertImages('image', 'xlink:href'); convertImages('img', 'src'); convertUses(); dedupImageDefs(); 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) { // Null prototype: keyed by font URLs parsed from the diagram's extFonts this.cachedFonts = Object.create(null); } 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) { var cached = this.cachedFonts[url]; if (cached == null) { // Marks font as being fetched, concurrent calls wait // for the result via the callbacks in the waiters list var waiters = []; this.cachedFonts[url] = waiters; 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'; } var fontDone = mxUtils.bind(this, function(uri) { this.cachedFonts[url] = uri; waiting--; finish(); for (var k = 0; k < waiters.length; k++) { waiters[k](); } }); this.mapFontUrl(mime, url, mxUtils.bind(this, function(realMime, realUrl) { // LATER: Remove cache-control header this.loadUrl(realUrl, function(uri) { fontDone(uri); }, function(err) { // LATER: handle error, keeps external font URL fontDone(url); }, true, null, 'data:' + realMime + ';charset=utf-8;base64,'); })); } else if (typeof cached != 'string') { // Waits for the font that is being fetched in another call waiting++; cached.push(function() { waiting--; finish(); }); } }))(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 the external fonts in the given font lookup. Default lookup * is Graph.customFontElements. */ Editor.prototype.embedExtFonts = function(callback, lookup) { var extFonts = this.graph.getCustomFonts(lookup); 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("' + Graph.escapeCssString(fontUrl) + '");\n'); googleCssDone(); })); } else { content.push(this.cachedGoogleFonts[fontUrl] + '\n'); } } else { content.push('@font-face {' + 'font-family: "' + Graph.escapeCssString(fontName) + '";' + 'src: url("' + Graph.escapeCssString(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 = Editor.getDefaultPageBackgroundColor(); } 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') + ':'); 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') + ':'); 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')); typeSelect.appendChild(boolOption); var clrOption = document.createElement('option'); clrOption.setAttribute('value', 'color'); mxUtils.write(clrOption, mxResources.get('color')); typeSelect.appendChild(clrOption); var enumOption = document.createElement('option'); enumOption.setAttribute('value', 'enum'); mxUtils.write(enumOption, mxResources.get('enum')); typeSelect.appendChild(enumOption); var floatOption = document.createElement('option'); floatOption.setAttribute('value', 'float'); mxUtils.write(floatOption, mxResources.get('float')); typeSelect.appendChild(floatOption); var intOption = document.createElement('option'); intOption.setAttribute('value', 'int'); mxUtils.write(intOption, mxResources.get('int')); typeSelect.appendChild(intOption); var strOption = document.createElement('option'); strOption.setAttribute('value', 'string'); mxUtils.write(strOption, mxResources.get('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') + ':'); 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') + ' (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'), 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}, {name: 'footerSize', dispName: 'Footer Size', type: 'float', min: 0, defVal: 0}, // primary shows the color in the style panel (see getCustomColors) // for cells that actually have a footer {name: 'footerColor', dispName: 'Footer Color', type: 'color', defVal: null, primary: true, isVisible: function(state, format) { return mxUtils.getValue(state.style, 'footerSize', 0) > 0; }} ]; mxCellRenderer.defaultShapes['ellipse'].prototype.customProperties = [ {name: 'centerRadius', dispName: 'Center Radius', type: 'float', min: 0, defVal: 0}, // primary shows the color in the style panel (see getCustomColors) // for ellipses that actually have a center circle (UML final states) {name: 'centerColor', dispName: 'Center Color', type: 'color', defVal: null, primary: true, isVisible: function(state, format) { return mxUtils.getValue(state.style, 'centerRadius', 0) > 0; }} ]; 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}, {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; 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 }, {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; mxCellRenderer.defaultShapes['cylinder'].prototype.customProperties = [ {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; mxCellRenderer.defaultShapes['cylinder3'].prototype.customProperties = [ {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; mxCellRenderer.defaultShapes['umlState'].prototype.customProperties = [ {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; 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 }, {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; 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['note2'].prototype.customProperties = [ {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; 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['wedgeCallout'].prototype.customProperties = [ {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE}, {name: 'tipX', dispName: 'Tip X', type: 'float', min:-100, max:100, defVal: -0.25}, {name: 'tipY', dispName: 'Tip Y', type: 'float', min:-100, max:100, defVal: 1}, {name: 'base', dispName: 'Callout Width', type: 'float', min:0, defVal: 20} ]; 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'}] }, {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; 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: 'footerSize', dispName: 'Footer Size', type: 'float', min: 0, defVal: 0}, {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}, {name: 'footerSize', dispName: 'Footer Size', type: 'float', min: 0, defVal: 0}, // primary shows the color in the style panel (see getCustomColors) // for cells that actually have a footer {name: 'footerColor', dispName: 'Footer Color', type: 'color', defVal: null, primary: true, isVisible: function(state, format) { return mxUtils.getValue(state.style, 'footerSize', 0) > 0; }} ]; 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}, {name: 'boundedLbl', dispName: 'Bounded Label', type: 'bool', defVal: false} ]; 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}, {name: 'lifelineMirror', dispName:'Mirror Head', type:'bool', defVal:false} ]; 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.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) ? mxUtils.safeDecodeURIComponent(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); select.style.boxSizing = 'border-box'; select.style.height = '100%'; select.style.padding = '0'; select.style.margin = '0'; select.style.border = '0'; 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 = ''; // Map the null sentinel back to actual null so op.val == ... matches. var effectiveVal = (select[select.selectedIndex] == nullOption || select.value == nullValue) ? null : select.value; for (let i = 0; i < pEnumList.length; i++) { let op = pEnumList[i]; if (op.val == effectiveVal) { 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(mxUtils.safeDecodeURIComponent(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 = mxUtils.safeDecodeURIComponent(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; // LOCAL type: setInputVal can run twice for one edit // (enter-commit + the blur listener) — mutating the // row's pType would make the second run misparse the // value (an empty value came back as 0, not auto). var type = pType; // A cleared value means AUTO for auto-capable properties // (jettySize) — only an explicitly entered 0 means 0 — // and 0 for the other numeric types. inputVal = inputVal.trim().length == 0 && type != 'string' && type != 'numbers'? ((prop.allowAuto) ? 'auto' : 0) : inputVal; if (prop.allowAuto) { if (inputVal.trim != null && inputVal.trim(). toLowerCase() == 'auto') { inputVal = 'auto'; type = '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 { // Auto is the property's default: REMOVE the key // (null unsets it in setCellStyles) instead of // writing jettySize=auto into the style. newVal = (type == 'numbers') ? inputVal.match(/\d*\.?\d+/g).map(Number).join(' ') : (inputVal === 'auto' && prop.allowAuto && prop.defVal == 'auto') ? null : encodeURIComponent((type == '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; var model = graph.getModel(); var cellMeta = []; var labelOnly = cells.length > 0; for (var i = 0; i < cells.length; i++) { var isEdgeLabel = model.isVertex(cells[i]) && model.isEdge(model.getParent(cells[i])); var cellStyle = graph.getCellStyle(cells[i]); var isImage = cellStyle != null && cellStyle.shape == 'image'; var geo = graph.getCellGeometry(cells[i]); // Edge labels with a size are shapes that use the fill and // stroke colors (see updateEdgeLabelShape) while zero-size // edge labels and images use the label background and // border colors cellMeta.push({isLabel: isImage || (isEdgeLabel && (geo == null || geo.width <= 0 || geo.height <= 0))}); if (!isEdgeLabel && !isImage) { labelOnly = false; } } for (var i = 0; i < cells.length; i++) { var meta = cellMeta[i]; if (!labelOnly && meta.isLabel) { continue; } var fillKey = (meta.isLabel) ? mxConstants.STYLE_LABEL_BACKGROUNDCOLOR : mxConstants.STYLE_FILLCOLOR; var strokeKey = (meta.isLabel) ? mxConstants.STYLE_LABEL_BORDERCOLOR : mxConstants.STYLE_STROKECOLOR; var style = model.getStyle(cells[i]); if (style != null && typeof style !== 'string') { style = String(style); } if (colorset != null) { if (!mxEvent.isShiftDown(evt)) { if (colorset['fill'] == '' || colorset['fill'] == null) { style = mxUtils.setStyle(style, fillKey, null); } else { style = mxUtils.setStyle(style, fillKey, colorset['fill']); } if (!meta.isLabel) { 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)) && model.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['stroke'] == null) { style = mxUtils.setStyle(style, strokeKey, null); } else { style = mxUtils.setStyle(style, strokeKey, colorset['stroke']); } } } else { style = mxUtils.setStyle(style, fillKey, null); style = mxUtils.setStyle(style, strokeKey, null); if (!meta.isLabel) { style = mxUtils.setStyle(style, mxConstants.STYLE_GRADIENTCOLOR, null); } if (model.isVertex(cells[i])) { style = mxUtils.setStyle(style, mxConstants.STYLE_FONTCOLOR, null); } } model.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. */ // Uses a null prototype as the keys are untrusted, lowercased font // names so that names such as __proto__ or constructor are stored as // regular entries instead of resolving to inherited object members Graph.customFontElements = Object.create(null); /** * 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; }; /** * Escapes the given value for use inside a double-quoted CSS string, * such as a font-family name or a url(...) target. Font names and URLs * come from untrusted cell styles, so escaping the quote, backslash and * control characters (including newlines) keeps them inside the string * and prevents breaking out to inject CSS rules into exports or the * document stylesheet. */ Graph.escapeCssString = function(value) { return (value != null) ? String(value).replace(/[\x00-\x1f\x7f"\\]/g, function(chr) { return '\\' + chr.charCodeAt(0).toString(16) + ' '; }) : value; }; /** * 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: "' + Graph.escapeCssString(name) + '";\n' + 'src: url("' + Graph.escapeCssString(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, element is not // in the document for temporary font lookups if (entry != null && entry.url != url) { if (entry.elt.parentNode != null) { 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) { // Node name is lowercase in XML documents (eg. SVG export) var name = (elts[i].nodeName.toUpperCase() == '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'; /** * Copy on connect is enabled by default in the sketch theme. Use the * copyOnConnect configuration key to override. */ mxConnectionHandler.prototype.createTarget = urlParams['sketch'] == '1'; /** * 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]) + '\n' + mxResources.get('shiftClickShowOnly', [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)) { var otherTags = []; for (var j = 0; j < allTags.length; j++) { if (allTags[j] !== tag) { otherTags.push(allTags[j]); } } // If already showing only this tag, show all var allOthersHidden = otherTags.length > 0; for (var j = 0; j < otherTags.length; j++) { if (mxUtils.indexOf(graph.hiddenTags, otherTags[j]) < 0) { allOthersHidden = false; break; } } if (allOthersHidden && mxUtils.indexOf(graph.hiddenTags, tag) < 0) { graph.setHiddenTags([]); } else { graph.setHiddenTags(otherTags); } removeInvisibleSelectionCells(); graph.refresh(); } 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) in the given font lookup. * Default lookup is Graph.customFontElements. */ Graph.prototype.getCustomFonts = function(lookup) { lookup = (lookup != null) ? lookup : Graph.customFontElements; var fonts = this.extFonts; if (fonts != null) { fonts = fonts.slice(); } else { fonts = []; } for (var key in lookup) { var font = lookup[key]; fonts.push({name: font.name, url: font.url}); } return fonts; }; /** * Returns the CSS with external references for the custom fonts in * the given font lookup. Default lookup is Graph.customFontElements. */ Graph.prototype.getExtFontCss = function(lookup) { var extFonts = this.getCustomFonts(lookup); 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.escapeCssString( Graph.rewriteGoogleFontUrl(fontUrl)) + '");\n'; } else { postfix += '@font-face {\n' + 'font-family: "' + Graph.escapeCssString(fontName) + '";\n' + 'src: url("' + Graph.escapeCssString(fontUrl) + '");\n}\n'; } } return prefix + postfix; }; /** * 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, grid) { // Sizes the export crop to the rendered MathJax output rather than the // raw formula source. Export paths that build a temporary graph (and the // headless export backend) compute the crop before the labels have been // typeset, so the cached view bounds reflect the much wider source text // and the export gets excessive margins. Typesets synchronously (only if // the labels are not already typeset, to leave the live editor untouched) // and refreshes the math bounds so the base getSvg crops correctly. // [jgraph/drawio#5564] if (Editor.mathOutputSize && this.mathEnabled && this.container != null && document.body.contains(this.container) && typeof MathJax !== 'undefined' && typeof MathJax.typeset === 'function') { if (this.container.getElementsByTagName('mjx-container').length == 0) { try { MathJax.typeset([this.container]); } catch (e) { // Fonts may not be loaded yet; bounds fall back to source size } } this.refreshMathBounds(); } 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 extFontCss = (incExtFonts) ? this.getExtFontCss() : ''; // Adds external fonts if (extFontCss.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'); style.appendChild(svgDoc.createTextNode(extFontCss)); 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 = mxUtils.safeDecodeURIComponent(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( Graph.flattenAnimationActions(link.actions), null, cell); } } }; /** * Inlines any {animation: {steps: [...]}} action by replacing it with its * `steps` array (recursively). The wrapper is a UI/editing convenience — * at execution time the steps are just regular custom actions. */ Graph.flattenAnimationActions = function(actions) { if (!Array.isArray(actions)) { return actions; } var out = []; for (var i = 0; i < actions.length; i++) { var a = actions[i]; if (a != null && a.animation != null && Array.isArray(a.animation.steps)) { var sub = Graph.flattenAnimationActions(a.animation.steps); for (var j = 0; j < sub.length; j++) { out.push(sub[j]); } } else { out.push(a); } } return out; }; /** * Animates `container.scrollLeft / scrollTop` from (sx, sy) to * (tx, ty) over `duration` ms with an ease-out cubic curve. * Hand-rolled because the browser's `scrollTo({behavior:'smooth'})` * is unreliable when called immediately after a synchronous * scroll-mutating function (which is what `fitWindow` and * `scrollCellToVisible` are) — Chromium often consolidates the * sequence into a single jump. */ Graph.smoothScrollContainer = function(container, sx, sy, tx, ty, duration) { duration = duration || 600; if (sx === tx && sy === ty) return; container.scrollLeft = sx; container.scrollTop = sy; var startTime = (typeof performance != 'undefined' && performance.now != null) ? performance.now() : Date.now(); var dx = tx - sx; var dy = ty - sy; var step = function(now) { var t = Math.min(1, (now - startTime) / duration); // Cubic ease-out: 1 - (1 - t)^3 — fast start, gentle settle. var eased = 1 - Math.pow(1 - t, 3); container.scrollLeft = sx + dx * eased; container.scrollTop = sy + dy * eased; if (t < 1) window.requestAnimationFrame(step); }; window.requestAnimationFrame(step); }; /** * Sets a CSS `transition: transform …` on the SVG group used in * chromeless mode. The next change to the group's `transform` * attribute (driven by updateCssTransform) will animate. The * transition is auto-cleared on `transitionend` and via a * setTimeout fail-safe (in case the new transform equals the old * and `transitionend` never fires). */ Graph.applyTransformTransition = function(graph, duration) { duration = duration || 600; var pane = graph.view.getDrawPane(); var node = (pane != null) ? pane.parentNode : null; if (node == null) return; var prev = node.style.transition; node.style.transition = 'transform ' + duration + 'ms cubic-bezier(0.16, 1, 0.3, 1)'; // Mark that the *next* transform change (the one this smooth step // is about to make via updateCssTransform) is the intended one to // animate. updateCssTransform consumes this flag; any other // transform update (toolbar zoom/fit, wheel zoom, …) finds it unset // and strips the transition so the viewport snaps instantly — the // easing must not bleed onto user-driven viewport changes. graph.armTransformTransition = true; var clear = function() { node.style.transition = prev || ''; graph.armTransformTransition = false; node.removeEventListener('transitionend', clear); }; node.addEventListener('transitionend', clear); window.setTimeout(clear, duration + 100); }; /** * Chromeless-mode equivalent of `fitWindow`. The lightbox container * is scrollable (`overflow:auto`), so we drive layout the same way * the editor's `fitWindow` does — keep `view.translate` stable, * change only the scale, and position the bounds via * `container.scrollLeft/scrollTop`. * * Why scale-only: * • `view.translate` defines what graph point lives at the SVG * element's (0, 0). Changing it shifts the "scroll origin", * so the diagram's top-left would no longer correspond to * scrollLeft=0. Users expect the scrollbars' top-left to * always show the top-left of the diagram, regardless of * where the viewbox is centered. * • Scale changes via CSS transform are visually instant and * don't require re-rendering cells. * * `view.setScale` routes through `viewStateChanged → validate + * sizeDidChange`. The latter resizes the SVG root's * `minWidth/minHeight` based on `getGraphBounds()` (which, in * useCssTransforms mode, returns `(graphBounds + currentTranslate) * * currentScale`). So the container's scrollable area grows / * shrinks with the scale, and the existing `currentTranslate` * keeps the diagram's top-left anchored to the same SVG point. */ Graph.prototype.fitBoundsCssTransform = function(bounds, border) { var b = (border != null) ? border : 10; // Clamp the requested viewbox to the diagram area — "best // effort". The bits past the diagram are empty space we can't // scroll into, so they'd just leave the viewbox off-centre at // the edges. Intersecting with the diagram gives a viewbox we // can actually position. var gb = this.view.graphBounds; var areaLeft = gb.x; var areaTop = gb.y; var areaRight = gb.x + gb.width; var areaBottom = gb.y + gb.height; var vbLeft = Math.max(areaLeft, bounds.x); var vbTop = Math.max(areaTop, bounds.y); var vbRight = Math.min(areaRight, bounds.x + bounds.width); var vbBottom = Math.min(areaBottom, bounds.y + bounds.height); var vbWidth = Math.max(1, vbRight - vbLeft); var vbHeight = Math.max(1, vbBottom - vbTop); var cw = this.container.clientWidth - b; var ch = this.container.clientHeight - b; var scale = Math.floor(20 * Math.min( cw / vbWidth, ch / vbHeight)) / 20; // Editor-style layout for the lightbox: translate so the // diagram's top-left maps to SVG (0, 0). After `sizeDidChange` // runs from `viewStateChanged`, the SVG element's `minWidth/ // minHeight` becomes `(gb.width * scale + 2 * getBorder())` — // exactly the diagram at the current scale plus the graph's // border. From the user's perspective: // • `scrollLeft = 0` → top-left of the diagram (not the // centred-with-padding layout `chromelessResize` sets up). // • Scrollable extent = diagram size at current scale. // • Panning happens entirely via `container.scrollLeft/Top`. // // The translate becomes a stable, scale-independent value // across viewbox actions (always `-gb.x`, `-gb.y`), so repeated // viewbox clicks at different zoom levels don't shift the // scroll-origin's meaning. this.view.scaleAndTranslate(scale, -gb.x, -gb.y); // With the new translate, graph (gx, gy) renders at SVG position // `(gx - gb.x) * scale`. Centre the clamped viewbox in the // viewport using the same math as `mxGraph.fitWindow`'s // hasScrollbars branch. var idealLeft = (vbLeft - gb.x) * scale - Math.max((cw - vbWidth * scale) / 2 + b / 2, 0); var idealTop = (vbTop - gb.y) * scale - Math.max((ch - vbHeight * scale) / 2 + b / 2, 0); // Scrollable extent: SVG element's minWidth is // `gb.width * scale + 2 * graphBorder`. Clamp to [0, max] so // the visible viewport stays within the diagram + border area. var graphBorder = this.getBorder(); var svgWidth = gb.width * scale + 2 * graphBorder; var svgHeight = gb.height * scale + 2 * graphBorder; var maxLeft = svgWidth - this.container.clientWidth; var maxTop = svgHeight - this.container.clientHeight; this.container.scrollLeft = (maxLeft < 0) ? 0 : Math.max(0, Math.min(idealLeft, maxLeft)); this.container.scrollTop = (maxTop < 0) ? 0 : Math.max(0, Math.min(idealTop, maxTop)); }; /** * Smooth variant of `fitWindow`. * * • Chromeless — `fitBoundsCssTransform` changes scale via CSS * transform and snaps scrollLeft/scrollTop to centre the * bounds. We arm a CSS `transition: transform` so the scale * animation is visually smooth, and tween scrollLeft/scrollTop * in parallel with `smoothScrollContainer`. * * • Editor — mxGraphView re-renders at each scale change, so we * can't tween scale visually. Snap the zoom via fitWindow, then * tween scrollLeft / scrollTop with `smoothScrollContainer`. * * The optional `done` callback fires once the transition has run its * full duration so callers (the action chain) can wait for it. */ /** * Returns true once the graph has been torn down — its container is null * (mxGraph.destroy nulls it) or the destroyed flag is set. Deferred * animation callbacks (loop iterations, smooth-transition timeouts, the * executeCustomActions chain) check this so they bail out instead of * dereferencing a null container after the lightbox / presentation view * closes mid-playback. */ Graph.prototype.isGraphTornDown = function() { return this.destroyed === true || this.container == null; }; Graph.prototype.smoothFitWindow = function(bounds, border, done) { // Graph may have been torn down between scheduling and firing. if (this.isGraphTornDown()) { if (done != null) done(); return; } var duration = 600; var container = this.container; var startLeft = container.scrollLeft; var startTop = container.scrollTop; if (this.useCssTransforms) { Graph.applyTransformTransition(this, duration); this.fitBoundsCssTransform(bounds, border); } else { this.fitWindow(bounds, border); } // Capture target scroll set by fitBoundsCssTransform / fitWindow, // then animate from start → target in parallel with the CSS // transform transition (chromeless) or with the snapped zoom // (editor). Graph.smoothScrollContainer(container, startLeft, startTop, container.scrollLeft, container.scrollTop, duration); // The scroll tween and the CSS transform transition both run for // `duration` ms, so the transition is complete after `duration`. if (done != null) { window.setTimeout(done, duration); } }; /** * Scrolls the given cell to the centre of the viewport in * chromeless mode. Equivalent to `scrollCellToVisible(cell, true)` * for the lightbox case. * * Normalizes `view.translate` to `(-gb.x, -gb.y)` so the * scrollable area equals the diagram at the current scale and * `scrollLeft = 0` maps to the diagram's top-left — same editor- * style layout that `fitBoundsCssTransform` produces. Without this, * if a viewbox action ran before, the translate was already * normalized; if the user clicked scroll right after lightbox * load, the translate was still the `chromelessResize` centred * value and the math would put the scroll target in the wrong * place. Normalizing makes the behaviour scale-invariant and * independent of which action came first. */ Graph.prototype.scrollCellToVisibleCssTransform = function(cell, border) { var state = this.view.getState(cell); if (state == null) return; var gb = this.view.graphBounds; var s = this.view.scale; // Normalize translate (no scale change). `scaleAndTranslate` // routes through `viewStateChanged → validate + sizeDidChange` // so the SVG element resizes to `gb.width * s + 2 * border`. if (this.view.translate.x !== -gb.x || this.view.translate.y !== -gb.y) { this.view.scaleAndTranslate(s, -gb.x, -gb.y); } // state.x/y/w/h are in graph coords (validate runs at scale=1, // translate=0 in useCssTransforms mode). With translate now // `-gb`, screen position of graph (gx, gy) = `(gx - gb.x) * s`. var cw = this.container.clientWidth; var ch = this.container.clientHeight; var graphBorder = this.getBorder(); var maxLeft = gb.width * s + 2 * graphBorder - cw; var maxTop = gb.height * s + 2 * graphBorder - ch; var left, top; if (border != null) { // Bring the cell into view with `border` px of breathing room // on every side, scrolling the minimum needed instead of // centring. `border` is in screen px (like the viewbox border). var cellLeft = (state.x - gb.x) * s; var cellTop = (state.y - gb.y) * s; var cellRight = (state.x + state.width - gb.x) * s; var cellBottom = (state.y + state.height - gb.y) * s; left = this.container.scrollLeft; top = this.container.scrollTop; if (cellLeft - border < left) { left = cellLeft - border; } else if (cellRight + border > left + cw) { left = cellRight + border - cw; } if (cellTop - border < top) { top = cellTop - border; } else if (cellBottom + border > top + ch) { top = cellBottom + border - ch; } } else { // Centre the cell (default behaviour). left = (state.x + state.width / 2 - gb.x) * s - cw / 2; top = (state.y + state.height / 2 - gb.y) * s - ch / 2; } this.container.scrollLeft = (maxLeft < 0) ? 0 : Math.max(0, Math.min(left, maxLeft)); this.container.scrollTop = (maxTop < 0) ? 0 : Math.max(0, Math.min(top, maxTop)); }; /** * Editor (non-chromeless) scroll-to-visible with an optional `border`. * With no border this is just the base `scrollCellToVisible`. With a * border (screen px) the cell's rect is expanded by that much on every * side and scrolled minimally into view, so the cell lands with that * much breathing room instead of flush against the viewport edge. */ Graph.prototype.scrollCellToVisibleBorder = function(cell, border) { if (border == null) { this.scrollCellToVisible(cell); return; } var state = this.view.getState(cell); if (state == null) return; // Same rect construction as the base scrollCellToVisible, expanded // by `border` px on every side before scrolling it into view. var x = -this.view.translate.x; var y = -this.view.translate.y; this.scrollRectToVisible(new mxRectangle( x + state.x - border, y + state.y - border, state.width + 2 * border, state.height + 2 * border)); }; /** * Smooth variant of `scrollCellToVisible`. Both chromeless and * editor paths now use `smoothScrollContainer` because * `scrollCellToVisibleCssTransform` mutates `scrollLeft/scrollTop`, * not `view.translate`, in the lightbox. * * `border` (screen px, optional) keeps that much breathing room around * the cell instead of centring it — see `scrollCellToVisibleCssTransform` * / `scrollCellToVisibleBorder`. * * The optional `done` callback fires once the transition has run its * full duration so callers (the action chain) can wait for it. */ Graph.prototype.smoothScrollCellToVisible = function(cell, border, done) { // Graph may have been torn down between scheduling and firing. if (this.isGraphTornDown()) { if (done != null) done(); return; } var duration = 600; var container = this.container; var startLeft = container.scrollLeft; var startTop = container.scrollTop; if (this.useCssTransforms) { this.scrollCellToVisibleCssTransform(cell, border); } else { this.scrollCellToVisibleBorder(cell, border); } Graph.smoothScrollContainer(container, startLeft, startTop, container.scrollLeft, container.scrollTop, duration); if (done != null) { window.setTimeout(done, duration); } }; /** * Transient toggle visibility — flips each cell's DOM opacity between * 0 and 1 without touching the model. Used by custom-action / animation * `toggle` steps when `transient !== false` (the default). Effects * are reverted by `graph.refresh()` since they only live in the SVG * group's inline `style.opacity`. */ Graph.prototype.toggleCellsTransient = function(cells) { var nodes = this.getNodesForCells(cells); for (var i = 0; i < nodes.length; i++) { var cur = nodes[i].style.opacity; // Default opacity is '' (full) — treat empty as visible. var visible = (cur === '' || cur === null || parseFloat(cur) > 0); nodes[i].style.opacity = visible ? 0 : 1; } }; /** * Transient style setter — mutates `state.style[key]` and redraws the * affected cell states without touching the model. Subsequent view * validation (zoom / pan / refresh) re-reads from `cell.style` and * reverts the change, which is exactly what we want for animation / * custom-action steps that should leave the saved diagram untouched. */ Graph.prototype.setCellStylesTransient = function(key, value, cells) { for (var i = 0; i < cells.length; i++) { var state = this.view.getState(cells[i]); if (state != null && state.style != null) { if (value == null) { delete state.style[key]; } else { state.style[key] = value; } // `apply` reads styled props into the shape's cached // fields; `redraw` repaints. Same pair the regular // `setCellStyles` path uses inside its endUpdate hook. if (state.shape != null) { state.shape.apply(state); state.shape.redraw(); } if (state.text != null) { state.text.apply(state); state.text.redraw(); } } } }; /** * Transient style toggle — flips a key between `defaultValue` and its * opposite (typically '0' ↔ '1') on each cell's state.style without * model mutation. Same revert-on-refresh semantics as * setCellStylesTransient. */ Graph.prototype.toggleCellStylesTransient = function(key, defaultValue, cells) { if (defaultValue == null) defaultValue = '0'; for (var i = 0; i < cells.length; i++) { var state = this.view.getState(cells[i]); if (state != null && state.style != null) { // Match the model-mutating mxGraph.toggleCellStyles // semantics: truthy current → 0, falsy → 1 (so the key // flips between '0' and '1' across repeated calls). // Unlike the model path — which reads the first cell's // style and applies the same value to all cells — this // path toggles each cell independently, which matches // what users intuitively expect from a multi-select // toggle. The model path keeps its legacy semantics // when invoked via `transient: false`. var next = (mxUtils.getValue(state.style, key, defaultValue)) ? '0' : '1'; state.style[key] = next; if (state.shape != null) { state.shape.apply(state); state.shape.redraw(); } if (state.text != null) { state.text.apply(state); state.text.redraw(); } } } }; /** * 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(); } }); // Custom-link actions (a user clicking a cell link — `cell` is the // clicked cell) default to the legacy model-mutating visibility // behavior so they can reveal model-hidden cells/layers/groups, the // long-standing interactive-diagram contract that predates step // animations. A transient (opacity-only) toggle can't reveal a cell // with visible="0" — it has no rendered state/DOM node, so the // action silently no-ops. Animation steps and the dialog preview // pass cell == null and stay transient by default (no model // mutation, no collab sync, no undo). Either side overrides per // action with an explicit `transient` flag. var defaultTransient = (cell == null); var isTransient = function(params) { return (params.transient != null) ? params.transient : defaultTransient; }; var waitAndExecute = mxUtils.bind(this, function() { if (waitCounter > 0) { waitCounter--; } if (waitCounter == 0) { executeNextAction() } }); var dispatchNextAction = mxUtils.bind(this, function() { // Bail out if the graph was torn down (e.g. presentation / // lightbox view closed) mid-animation — a deferred step must // not dispatch into a null container / destroyed view. Stops // the chain for ANY action type, not just the smooth viewport // transitions that read this.container directly. if (this.isGraphTornDown()) { this.executingCustomActions = false; this.stoppingCustomActions = false; // Unwind the caller so a looping player resets its // `running` flag — its runStep then hits the same guard // and halts without re-dispatching. `done` is null for // fire-and-forget callers (e.g. custom-link clicks). if (done != null) { done(); } return; } if (index < actions.length) { // Each iteration processes one step. The `immediate: true` // step flag opts into running in parallel with the previous // step: when set, we keep batching steps in the same call // and they share one `waitCounter`. The chain advances once // the whole batch finishes (i.e. all blocking effects in // any step of the batch have completed). The first step is // always processed unconditionally; only `immediate` on // subsequent steps takes effect (the field is ignored on // the first step in `actions`). do { 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)) { // A failed link (customLinkClicked shows the error // dialog and returns false, e.g. pageNotFound) // aborts the chain. The abort must reset the // execution flags like the teardown guard above — // leaving executingCustomActions latched sends // every later call into the already-executing // branch, silently disabling all custom links // until the page is reloaded // [jgraph/drawio-desktop#2161]. done() still fires // so a waiting animation player unwinds instead of // hanging on a step that never completes. if (!this.customLinkClicked(action.open, cell)) { this.executingCustomActions = false; this.stoppingCustomActions = false; if (done != null) { done(); } 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); } // Animated transition to an arbitrary target opacity. Sits // between the existing `opacity` (instant) and `fadeIn`/ // `fadeOut` (animated, hardcoded 0 or 1) — picks up where // they leave off for partial-opacity effects. if (action.fadeTo != null && action.fadeTo.value != null) { waitCounter++; var fadeNodes = this.getNodesForCells( this.getCellsForAction(action.fadeTo, true)); var fadeDelay = (stop) ? 0 : (action.fadeTo.delay != null ? action.fadeTo.delay : 1000); Graph.setTransitionForNodes(fadeNodes, 'all ' + fadeDelay + 'ms ease-in-out'); Graph.setOpacityForNodes(fadeNodes, action.fadeTo.value); window.setTimeout(function() { Graph.setTransitionForNodes(fadeNodes, null); waitAndExecute(); }, fadeDelay); } // Toggles SVG flow animation on edge path elements. The // CSS class is installed once at module load (see // Editor.installAnimationStyles). if (action.flow != null) { var flowCells = this.getCellsForAction(action.flow, true); Editor.toggleFlowAnimation(this, flowCells, action.flow.start === false ? 'stop' : action.flow.start === true ? 'start' : 'toggle'); } 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)); } // Visibility / style actions. The default is context- // dependent (see `isTransient` / `defaultTransient` // above): animation steps are transient (DOM-only, // reverted by the next `graph.refresh()`, never saved / // synced / undoable), while custom-link clicks mutate the // model — the legacy interactive-diagram behavior that can // reveal model-hidden cells / layers / groups. An explicit // `transient: true|false` on the params overrides either // default (hidden from the picker UI). if (action.toggle != null) { if (!isTransient(action.toggle)) { beginUpdate(); // Model path: resolve layers to the layer cell // itself so its `visible` is toggled (cascading // to descendants), not each child's flag. this.toggleCells(this.getCellsForAction( action.toggle, true, true)); } else { this.toggleCellsTransient( this.getCellsForAction(action.toggle, true)); } } if (action.show != null) { var temp = this.getCellsForAction(action.show, true); Graph.setOpacityForNodes(this.getNodesForCells(temp), 1); if (!isTransient(action.show)) { beginUpdate(); // Layers → the layer cell itself, so its `visible` // is set (cascading to children) instead of each // descendant's flag. this.setCellsVisible(this.getCellsForAction( action.show, true, true), true); } } if (action.hide != null) { var temp = this.getCellsForAction(action.hide, true); Graph.setOpacityForNodes(this.getNodesForCells(temp), 0); if (!isTransient(action.hide)) { beginUpdate(); this.setCellsVisible(this.getCellsForAction( action.hide, true, true), false); } } if (action.toggleStyle != null && action.toggleStyle.key != null) { var toggleStyleCells = this.getCellsForAction(action.toggleStyle, true); var defValue = (action.toggleStyle.defaultValue != null) ? action.toggleStyle.defaultValue : '0'; if (!isTransient(action.toggleStyle)) { beginUpdate(); this.toggleCellStyles(action.toggleStyle.key, defValue, toggleStyleCells); } else { this.toggleCellStylesTransient(action.toggleStyle.key, defValue, toggleStyleCells); } } if (action.style != null && action.style.key != null) { var styleCells = this.getCellsForAction(action.style, true); if (!isTransient(action.style)) { beginUpdate(); this.setCellStyles(action.style.key, action.style.value, styleCells); } else { this.setCellStylesTransient(action.style.key, action.style.value, styleCells); } } // 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) { var vb = action.viewbox; // Dynamic cell-bound viewbox: with a cells/tags/layers // selector present, the box derives from the union of // the resolved cells' bounds at execution time — the // link follows the cells as the diagram evolves — and // any static x/y/width/height is ignored. An // unresolvable selector (e.g. only hidden cells, no // state) skips the action, like scroll does. if (vb.cells != null || vb.tags != null || vb.layers != null) { var vbBounds = this.getBoundingBox( this.getCellsForAction(vb)); if (vbBounds != null) { // State bounds are screen coords in the editor // but graph coords in useCssTransforms mode // (validate runs at scale 1, translate 0) — // same normalization as fitDiagramToWindow. var vbScale = (this.useCssTransforms) ? 1 : this.view.scale; var vbTrans = (this.useCssTransforms) ? new mxPoint(0, 0) : this.view.translate; // Dynamic border is breathing room per side // (screen px, like scroll's border). The fit // implementations reserve the border once // across the axis (clientWidth - border) and // centre, which yields border/2 per side — // invisible for typical values after scale // quantization — so double it here. var vbBorder = (vb.border != null && vb.border !== '' && !isNaN(parseFloat(vb.border))) ? 2 * parseFloat(vb.border) : null; vb = {x: vbBounds.x / vbScale - vbTrans.x, y: vbBounds.y / vbScale - vbTrans.y, width: Math.max(1, vbBounds.width / vbScale), height: Math.max(1, vbBounds.height / vbScale), border: vbBorder, smooth: vb.smooth}; } else { vb = null; } } if (vb != null) { if (vb.smooth === true && !stop) { // Block the action chain until the smooth // transition finishes so consecutive viewbox / // scroll steps don't overrun each other. During // stop we fall through to the instant snap below. waitCounter++; this.smoothFitWindow(vb, vb.border, waitAndExecute); } else if (this.useCssTransforms) { // Regular fitWindow only zooms in chromeless mode // (no scrollbars to pan), so we recreate the pan // ourselves via fitBoundsCssTransform. this.fitBoundsCssTransform(vb, vb.border); } else { this.fitWindow(vb, vb.border); } } } if (cells.length > 0) { // Optional `scroll.border` (screen px) keeps that much // breathing room around the cell instead of centring // it. Only `scroll` carries it — select/highlight- // driven scrolls pass null (centre / base behaviour). var scrollBorder = (action.scroll != null && action.scroll.border != null && action.scroll.border !== '' && !isNaN(parseFloat(action.scroll.border))) ? parseFloat(action.scroll.border) : null; // `scroll.smooth: true` animates the scroll // instead of jumping the container. if (action.scroll != null && action.scroll.smooth === true && !stop) { // Block the action chain until the smooth scroll // finishes (see viewbox above). During stop we // fall through to the instant scroll below. waitCounter++; this.smoothScrollCellToVisible(cells[0], scrollBorder, waitAndExecute); } else if (this.useCssTransforms) { // Chromeless: use the CSS-transforms-aware // helper that updates view.translate AND // calls sizeDidChange (so the SVG element // resizes to fit the panned region — without // this the cell scrolls off-screen at // high-zoom levels). this.scrollCellToVisibleCssTransform(cells[0], scrollBorder); } else { this.scrollCellToVisibleBorder(cells[0], scrollBorder); } } 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++; // Honour a per-effect duration (total ms) authored via // the dialog's duration input or the legacy converter, // which store it nested under the effect key (e.g. // action.wipeIn.delay). executeAnimations wants a frame // count + per-frame interval (total ≈ frames * interval), // so keep the default frame count and derive the // interval. Falls back to legacy top-level action.steps / // action.delay, then executeAnimations' own defaults. var animFrames = (action.steps != null) ? action.steps : 30; var animInterval = action.delay; var animKeys = ['wipeIn', 'wipeOut', 'popIn', 'popOut']; var animDurMs = null; for (var ak = 0; ak < animKeys.length; ak++) { var av = action[animKeys[ak]]; if (av != null && av.delay != null) { animDurMs = (animDurMs == null) ? av.delay : Math.max(animDurMs, av.delay); } } if (animDurMs != null) { animInterval = animDurMs / animFrames; } this.executeAnimations(animations, waitAndExecute, (stop) ? 1 : animFrames, (stop) ? 0 : animInterval); } } while (index < actions.length && actions[index].immediate === true && !this.stoppingCustomActions); if (waitCounter == 0) { executeNextAction(); } else { endUpdate(); } } else { this.executingCustomActions = false; this.stoppingCustomActions = false; endUpdate(); if (done != null) { done(); } } }); // An action that throws (e.g. a broken action body) must not // leave executingCustomActions latched — that silently disables // all custom links until the page is reloaded — nor leave the // model transaction open. Wraps every dispatch entry (initial, // recursive and the async waitAndExecute continuations) and // rethrows so a synchronous caller still surfaces the error // dialog (graph.customLinkClicked catches via handleError). var executeNextAction = mxUtils.bind(this, function() { try { dispatchNextAction(); } catch (e) { this.executingCustomActions = false; this.stoppingCustomActions = false; endUpdate(); throw e; } }); 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'); this.updateCustomLinkAction(mapping, action[name], 'layers'); } } }; /** * 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, layerCells) { // `action.tagsMatch` opts into OR semantics (cell needs ANY of // the listed tags). Default stays AND for back-compat with // existing diagrams. var tagMode = (action.tagsMatch === 'or') ? 'or' : 'and'; // Union of cells from `cells` and `tags`. De-dup by id so a // cell that matches both selectors doesn't get the action // applied twice. var seen = {}; var union = []; var merge = function(cells) { for (var i = 0; i < cells.length; i++) { var id = cells[i].id; if (!seen[id]) { seen[id] = true; union.push(cells[i]); } } }; merge(this.getCellsById(action.cells)); merge(this.getCellsForTags(action.tags, null, layers, false, tagMode)); // `layerCells` resolves the action.layers selector to the layer cells // themselves rather than their descendants. The transient opacity // path fades descendants; the model-visibility path toggles the layer // cell's own `visible` (matching the Layers panel), which cascades to // every descendant at render time. merge(layerCells ? this.getLayerCells(action.layers) : this.getCellsForLayers(action.layers)); // Final step: subtract every cell in `excludeCells` from the // union. `'*'` in the exclude list means "exclude everything", // which collapses the result to []. This mirrors the wildcard // semantics on `cells`. if (action.excludeCells != null && action.excludeCells.length > 0) { var excludeIds = {}; var excludeAll = false; for (var i = 0; i < action.excludeCells.length; i++) { if (action.excludeCells[i] === '*') { excludeAll = true; break; } excludeIds[action.excludeCells[i]] = true; } if (excludeAll) { return []; } var filtered = []; for (var i = 0; i < union.length; i++) { if (!excludeIds[union[i].id]) filtered.push(union[i]); } return filtered; } return union; }; /** * 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; }; /** * Resolves an array of layer cell IDs to every descendant cell in * those layers. Layers are top-level children of the model root; we * skip the layer cell itself (so the action applies only to its * contents, not the implicit container). Used by getCellsForAction * to support "Select layers" on the cells selector. */ Graph.prototype.getCellsForLayers = function(layerIds) { var result = []; if (Array.isArray(layerIds)) { for (var i = 0; i < layerIds.length; i++) { var layer = this.model.getCell(layerIds[i]); if (layer == null) continue; result = result.concat(this.model.filterDescendants( function(c) { return c != layer; }, layer)); } } return result; }; /** * Returns the layer cells named by the given IDs (the cells themselves, * not their descendants). Used by the model-visibility action path so a * `layers` selector toggles each layer's own `visible` attribute — * matching the Layers panel — rather than flipping every descendant. */ Graph.prototype.getLayerCells = function(layerIds) { var result = []; if (Array.isArray(layerIds)) { for (var i = 0; i < layerIds.length; i++) { var layer = this.model.getCell(layerIds[i]); if (layer != null) result.push(layer); } } 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, mode) { var result = []; var orMode = mode === 'or'; 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(' '); // OR mode matches as soon as one tag overlaps. AND mode // keeps the original requirement (every listed tag must // be present), short-circuiting once the count hits. if (orMode || tmp.length >= tagList.length) { var matchCount = 0; for (var j = 0; j < tmp.length && (matchCount < tagCount); j++) { if (lookup[tmp[j]] != null) { matchCount++; } } match = orMode ? matchCount > 0 : 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')); } return Math.ceil(this.svgShadowSize * this.svgShadowBlur); }; /** * 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['archimate4'] = [SHAPES_PATH + '/mxArchiMate3.js', SHAPES_PATH + '/mxArchiMate4.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'); // Flex column so the footer buttons stay pinned at the bottom and the // content area scrolls if the dialog is capped by viewport max-height. div.style.display = 'flex'; div.style.flexDirection = 'column'; div.style.minHeight = '0'; var scrollWrapper = document.createElement('div'); scrollWrapper.style.flex = '1 1 auto'; scrollWrapper.style.minHeight = '0'; scrollWrapper.style.overflowY = 'auto'; div.appendChild(scrollWrapper); var title = document.createElement('h3'); mxUtils.write(title, titleText || mxResources.get('print')); scrollWrapper.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'); allPagesRadio.setAttribute('id', 'gePrintDlgAllPages'); var allPagesRow = document.createElement('div'); allPagesRow.className = 'geDialogCheckRow'; allPagesRow.appendChild(allPagesRadio); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgAllPages'); mxUtils.write(span, mxResources.get('allPages')); 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'); pagesRadio.setAttribute('id', 'gePrintDlgPages'); var pagesRow = document.createElement('div'); pagesRow.className = 'geDialogCheckRow'; pagesRow.appendChild(pagesRadio); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgPages'); mxUtils.write(span, mxResources.get('pages') + ':'); pagesRow.appendChild(span); 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.className = 'geBtn'; currPage.setAttribute('title', mxResources.get('currentPage')); currPage.style.marginLeft = '4px'; currPage.style.maxWidth = '100px'; pagesRow.appendChild(currPage); pagesSection.appendChild(pagesRow); if (pageCount > 1) { scrollWrapper.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.setAttribute('id', 'gePrintDlgSelectionOnly'); selectionOnlyRadio.style.marginRight = '8px'; if (graph.isSelectionEmpty()) { selectionOnlyRadio.setAttribute('disabled', 'disabled'); } if (graph.isEnabled()) { selectionSection.appendChild(selectionOnlyRadio); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgSelectionOnly'); 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); scrollWrapper.appendChild(selectionWrapper); } } if (!editorUi.isPagesEnabled() || editorUi.lastPrintPagesRadioChecked) { pagesRadio.checked = true; } else if (!graph.isSelectionEmpty() && editorUi.lastPrintSelectionOnlyChecked) { selectionOnlyRadio.checked = true; } else { allPagesRadio.checked = true; } // Less common options live in a collapsed Advanced section. The size // section is the helper's content card, the options section is a // second card appended to the collapsible wrapper. var advanced = editorUi.addAdvancedSection(scrollWrapper); // --- Size section --- var sizeSection = advanced.content; // 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'); pageViewRadio.setAttribute('id', 'gePrintDlgPageView'); pageViewSection.appendChild(pageViewRadio); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgPageView'); 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'); cropRadio.setAttribute('id', 'gePrintDlgCrop'); cropSection.appendChild(cropRadio); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgCrop'); 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); // 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'; borderLabel.setAttribute('for', 'gePrintDlgBorder'); mxUtils.write(borderLabel, mxResources.get('borderWidth')); borderZoomRow.appendChild(borderLabel); var borderInput = document.createElement('input'); borderInput.setAttribute('type', 'number'); borderInput.setAttribute('min', '0'); borderInput.setAttribute('id', 'gePrintDlgBorder'); borderInput.style.width = '40px'; borderInput.value = (editorUi.lastPrintBorder != null) ? editorUi.lastPrintBorder : mxPrintPreview.prototype.pageMargin; borderZoomRow.appendChild(borderInput); var zoomLabel = document.createElement('label'); zoomLabel.setAttribute('for', 'gePrintDlgZoom'); zoomLabel.style.marginLeft = '12px'; zoomLabel.style.marginRight = '4px'; mxUtils.write(zoomLabel, mxResources.get('zoom')); borderZoomRow.appendChild(zoomLabel); var zoomInput = document.createElement('input'); zoomInput.setAttribute('id', 'gePrintDlgZoom'); 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.setAttribute('id', 'gePrintDlgGrid'); gridInput.style.marginRight = '8px'; gridInput.checked = (editorUi.lastPrintGrid != null) ? editorUi.lastPrintGrid : false; gridRow.appendChild(gridInput); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgGrid'); mxUtils.write(span, mxResources.get('grid')); gridRow.appendChild(span); optionsSection.appendChild(gridRow); // Shadows enabled var shadowsRow = document.createElement('div'); shadowsRow.className = 'geDialogCheckRow'; var shadowsInput = document.createElement('input'); shadowsInput.setAttribute('type', 'checkbox'); shadowsInput.setAttribute('id', 'gePrintDlgShadows'); shadowsInput.style.marginRight = '8px'; shadowsInput.checked = (editorUi.lastPrintShadow != null) ? editorUi.lastPrintShadow : false; shadowsRow.appendChild(shadowsInput); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgShadows'); mxUtils.write(span, mxResources.get('shadows')); shadowsRow.appendChild(span); if (!editorUi.isOffline() || mxClient.IS_CHROMEAPP) { shadowsRow.appendChild(editorUi.createHelpIcon( 'https://www.drawio.com/docs/manual/export/print-diagram/#shadows-in-print-and-pdf-export')); } // 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.setAttribute('id', 'gePrintDlgTransparent'); 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('label'); span.setAttribute('for', 'gePrintDlgTransparent'); mxUtils.write(span, mxResources.get('transparentBackground')); transparentRow.appendChild(span); optionsSection.appendChild(transparentRow); } // Include diagram var includeInput = document.createElement('input'); includeInput.setAttribute('type', 'checkbox'); includeInput.setAttribute('id', 'gePrintDlgInclude'); 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('label'); span.setAttribute('for', 'gePrintDlgInclude'); mxUtils.write(span, mxResources.get('includeCopyOfMyDiagram')); includeRow.appendChild(span); optionsSection.appendChild(includeRow); } // Notes as sticky note annotations, added by the local export // pipeline which only exists in the desktop app (tooltips and // links are always exported) var iconsInput = document.createElement('input'); iconsInput.setAttribute('type', 'checkbox'); iconsInput.setAttribute('id', 'gePrintDlgIcons'); iconsInput.style.marginRight = '8px'; iconsInput.checked = (editorUi.lastPrintIcons != null) ? editorUi.lastPrintIcons : false; if (fn != null && EditorUi.isElectronApp) { var iconsRow = document.createElement('div'); iconsRow.className = 'geDialogCheckRow'; iconsRow.appendChild(iconsInput); var span = document.createElement('label'); span.setAttribute('for', 'gePrintDlgIcons'); mxUtils.write(span, mxResources.get('notes')); iconsRow.appendChild(span); optionsSection.appendChild(iconsRow); } advanced.wrapper.appendChild(optionsSection); // Buttons var buttons = document.createElement('div'); buttons.style.flex = '0 0 auto'; 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.lastPrintShadow = shadowsInput.checked; editorUi.lastPrintTransparent = transparentInput.checked; editorUi.lastPrintInclude = includeInput.checked; editorUi.lastPrintIcons = iconsInput.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.icons = iconsInput.checked && EditorUi.isElectronApp; 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); } // Async preview or print with spinner function doPreview(print) { try { editorUi.hideDialog(); // Only the print/preview path (fn == null) renders synchronously // here and needs the spinner. The export path (fn != null) issues // its own request with its own spinner (e.g. saveRequest), which // no-ops and silently drops the request if a spinner is already // active, so it must not run under updatingPreview if (fn != null) { preview(print); } else if (editorUi.spinner.spin(document.body, mxResources.get('updatingPreview'))) { window.setTimeout(function() { try { preview(print); editorUi.spinner.stop(); } catch (e) { editorUi.handleError(e); } }, 0); } } catch (e) { editorUi.handleError(e); } }; if (PrintDialog.previewEnabled && fn == null) { var previewBtn = mxUtils.button(mxResources.get('preview'), function() { doPreview(false); }); 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() { doPreview(true); }); 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; } } }; /** * Step-based animation engine. Plays a sequence of show/hide/flow/wait/ * setOpacity steps on a graph by manipulating DOM opacity, leaving the * model and each cell's original opacity/label styles untouched. * * Step grammar (one per line, blank lines and lines starting with # ignored): * * show CELL [fade|pop|wipe] [DURATION] reveal cell (default effect: wipe) * hide CELL [DURATION] fade cell out * flow CELL [start|stop] toggle flow animation on an edge * wait MS pause for MS milliseconds * setOpacity CELL VALUE [DURATION] transition cell opacity to VALUE (0–1) * * DURATION is optional milliseconds; when omitted, fades and setOpacity * default to Editor.animationFadeDelay (400 ms) and wipe/pop default to * the executeAnimations 30×30 = 900 ms preset. * * CELL is the cell id (or "*" / "all" for every shape). wipe extends * vertices left-to-right and edges along their path. fade cross-fades * opacity. pop scales in with a damped spring overshoot. setOpacity is * essentially fadeIn/fadeOut with a configurable target — animates from * each cell's current opacity to VALUE. */ Editor.parseAnimationScript = function(script) { var steps = []; if (script == null) { return steps; } // Parses a non-negative numeric token. Returns the value, or undefined // if the token is missing or not a valid number. var parseNumber = function(tok) { if (tok == null) { return undefined; } var n = parseFloat(tok); return (!isNaN(n) && n >= 0) ? n : undefined; }; var SHOW_EFFECTS = {fade: true, pop: true, wipe: true}; var lines = script.split('\n'); for (var i = 0; i < lines.length; i++) { var line = mxUtils.trim(lines[i]); if (line == '' || line.charAt(0) == '#') { continue; } var tokens = line.split(/\s+/); var step = {action: tokens[0], cell: tokens[1], arg: tokens[2]}; if (step.action == 'wait') { step.delay = parseFloat(step.cell); if (isNaN(step.delay) || step.delay < 0) { step.delay = 1000; } } else if (step.action == 'setOpacity') { // setOpacity CELL VALUE [DURATION] step.opacity = parseFloat(tokens[2]); if (isNaN(step.opacity)) { step.opacity = 1; } step.opacity = Math.max(0, Math.min(1, step.opacity)); step.duration = parseNumber(tokens[3]); step.arg = undefined; } else if (step.action == 'show') { // show CELL [fade|pop|wipe] [DURATION] // The third token can be an effect name OR a duration. If it // parses as a number and isn't a known effect, treat it as a // duration with the default (wipe) effect. if (step.arg != null && !SHOW_EFFECTS[step.arg]) { var asDur = parseNumber(step.arg); if (asDur != null) { step.duration = asDur; step.arg = undefined; } } if (step.duration == null) { step.duration = parseNumber(tokens[3]); } } else if (step.action == 'hide') { // hide CELL [DURATION] step.duration = parseNumber(tokens[2]); step.arg = undefined; } steps.push(step); } return steps; }; /** * Default fade duration in ms used when fading hidden cells back in or out. */ Editor.animationFadeDelay = 400; /** * True when two steps differ only by their cell reference and can run as * a single parallel batch (e.g. three "Fade In" picks on a multi-cell * selection). wait/flow steps don't batch — flow is instant anyway and * waits have no cell to fan out. Shared by the engine (batching) and the * dialog (UI grouping) so both stay in sync. */ Editor.canBatchAnimationSteps = function(a, b) { if (a.action != b.action) return false; if (a.action == 'wait') return false; if ((a.arg || '') != (b.arg || '')) return false; if ((a.opacity != null ? a.opacity : '') != (b.opacity != null ? b.opacity : '')) return false; if ((a.duration != null ? a.duration : '') != (b.duration != null ? b.duration : '')) return false; return true; }; /** * Converts a parsed legacy step (from parseAnimationScript) to a single * JSON action object using the keys already supported by * Graph.executeCustomActions. Returns null for unknown actions. * * The mapping preserves semantics: * show CELL fade → fadeIn (forces 0 → 1) * show CELL pop → popIn * show CELL → wipeIn (default wipe effect) * hide CELL → fadeOut (forces 1 → 0) * setOpacity X V → fadeTo with value: V (smooth from current → V) * wait MS → wait: MS * flow CELL start → flow with start: true */ Editor.convertLegacyAnimationStep = function(step) { if (step.action == 'wait') { return {wait: step.delay}; } var cellRef = (step.cell == 'all') ? '*' : step.cell; if (cellRef == null) { return null; } var sel = {cells: [cellRef]}; // The legacy engine defaulted fade/setOpacity to 400 ms (Editor. // animationFadeDelay); the new action handlers fall back to // fadeNodes' built-in 1000 ms. Set the duration explicitly during // conversion so old animations play at the same speed they used to. if (step.duration != null) { sel.delay = step.duration; } else if (step.action == 'show' && step.arg == 'fade') { sel.delay = Editor.animationFadeDelay; } else if (step.action == 'hide' || step.action == 'setOpacity') { sel.delay = Editor.animationFadeDelay; } if (step.action == 'show') { if (step.arg == 'fade') return {fadeIn: sel}; if (step.arg == 'pop') return {popIn: sel}; return {wipeIn: sel}; } if (step.action == 'hide') { return {fadeOut: sel}; } if (step.action == 'setOpacity') { sel.value = step.opacity; return {fadeTo: sel}; } if (step.action == 'flow') { var out = {cells: [cellRef]}; if (step.arg == 'start') out.start = true; else if (step.arg == 'stop') out.start = false; return {flow: out}; } return null; }; /** * Converts an old text-format animation script to the new JSON action * structure {animation: {steps: [...]}}. Prepends an explicit * {opacity: {cells: ["*"], value: 0}} step to recreate the implicit * hide-initial behavior the old engine had on by default — users who * don't want it can delete that first step in the editor. * * Consecutive non-`wait` steps are merged into a single step so they * run in parallel. This matches the original plugin's semantics: its * dispatcher fired show / hide / flow synchronously (no per-step * await), so consecutive shows visually overlapped and only `wait` * blocked the chain. The new JSON dispatcher awaits each step's * completion, so without merging, a legacy script * `show A fade; show B fade; wait 500` would play in ~1300 ms instead * of the legacy ~500 ms. */ Editor.convertLegacyAnimation = function(text) { var legacy = Editor.parseAnimationScript(text); var steps = [{opacity: {cells: ['*'], value: 0}}]; var current = null; // Flush the in-progress merge bucket onto the steps array. var flush = function() { if (current != null) { steps.push(current); current = null; } }; // Returns true if two converted action params can share a step — // same `delay`, `value`, `start` (anything that affects how the // action plays). When they differ, the steps stay separate to // preserve the original per-step behavior. var paramsMatch = function(a, b) { return a.delay === b.delay && a.value === b.value && a.start === b.start; }; // Adds one converted step to the merge bucket. If the new step // shares an action key with the bucket AND their params match // (e.g. both are `fadeIn` with delay 400), the cells are // appended to the existing key's cells array. Different action // keys (e.g. `fadeIn` + `wipeIn`) coexist in the same merged // step — both run in parallel via the multi-key dispatch in // executeCustomActions. Conflicting params (e.g. two `fadeIn`s // with different delays) flush the bucket so each preserves // its own timing. var merge = function(converted) { if (current == null) current = {}; for (var key in converted) { var incoming = converted[key]; if (current[key] != null && incoming != null && Array.isArray(current[key].cells) && Array.isArray(incoming.cells) && paramsMatch(current[key], incoming)) { // Same key + matching params — extend cells. for (var i = 0; i < incoming.cells.length; i++) { current[key].cells.push(incoming.cells[i]); } } else if (current[key] != null) { // Same key but params differ — flush and start a // new bucket so the previous timing is preserved. flush(); current = {}; current[key] = incoming; } else { current[key] = incoming; } } }; for (var i = 0; i < legacy.length; i++) { var converted = Editor.convertLegacyAnimationStep(legacy[i]); if (converted == null) continue; // `wait` blocks the chain — it always lives in its own step. if (converted.wait != null) { flush(); steps.push(converted); } else { merge(converted); } } flush(); return {animation: {steps: steps}}; }; /** * Returns the normalized {steps: [...], loop} structure for an animation * value stored on a cell, page root, or custom-link action. Accepts: * - a {steps: [...]} object (already normalized) * - a {animation: {steps: [...]}} action object (unwrapped) * - a JSON string of either of the above * - a legacy text script (auto-converted via convertLegacyAnimation) * - null / empty value (returns {steps: []}) * * `loop` is always normalized to a boolean — defaults to true when the * field is missing (matches the legacy plugin's chromeless behavior). */ Editor.parseAnimationData = function(value) { var normalize = function(data) { if (data == null) data = {}; if (!Array.isArray(data.steps)) data.steps = []; // Coerce to boolean — handles JSON 0/1, "true"/"false" strings, // missing field (defaults to true). `enabled` gates chromeless // autoplay; `loop` toggles repeat playback. data.loop = (data.loop == null) ? true : !!data.loop; data.enabled = (data.enabled == null) ? true : !!data.enabled; return data; }; if (value == null || value === '') { return normalize({steps: []}); } if (typeof value == 'string') { var trimmed = mxUtils.trim(value); // A leading { or [ means the text is meant as JSON. If it then // fails to parse it is malformed JSON, NOT a legacy script — so // let the SyntaxError propagate instead of silently routing it // to convertLegacyAnimation, which would discard the user's // steps and leave only its hide-all prepend. Callers reading // from storage guard against the throw; the dialog textarea // handler catches it and keeps the last valid data in place. if (trimmed.charAt(0) == '{' || trimmed.charAt(0) == '[') { return Editor.parseAnimationData(JSON.parse(trimmed)); } // Legacy text format (never starts with { or [). return normalize(Editor.convertLegacyAnimation(value).animation); } if (value.animation != null && value.animation.steps != null) { return normalize(value.animation); } if (Array.isArray(value.steps)) { return normalize(value); } return normalize({steps: []}); }; /** * Animation runner. Plays a step-based animation by delegating each step * to the existing Graph.executeCustomActions infrastructure — which * already supports parallel cell batches, sequential waits, and * cells/tags/excludeCells selectors. * * The player adds three things on top of executeCustomActions: * 1. Snapshot/restore of DOM opacity so stop() and loops are clean * 2. Loop support (re-run from step 0 when done) * 3. Accepts legacy text scripts (auto-converted on load) */ Editor.AnimationPlayer = function(graph, data) { this.graph = graph; this.data = Editor.parseAnimationData(data); this.running = false; this.cancelled = false; this.snapshot = null; }; /** * Wildcard cell reference that matches every vertex and edge in the * model. Stored as the string "*" in JSON `cells` arrays — the existing * Graph.getCellsById already handles this wildcard. Kept exported for * legacy callers that referenced it directly. */ Editor.ANIMATION_ALL = '*'; /** * Returns every vertex and edge in the model. Used by the dialog when * resolving "select all referenced cells" against the wildcard. */ Editor.collectAnimatableCells = function(model) { var cells = []; for (var id in model.cells) { var cell = model.cells[id]; if (model.isVertex(cell) || model.isEdge(cell)) { cells.push(cell); } } return cells; }; /** * Walks every step in this.data.steps and returns the union of cells they * touch (resolving wildcard / tag / excludeCells via getCellsForAction). * Used to snapshot DOM opacity before play so the diagram can be restored * cleanly on stop or between loop iterations. */ Editor.AnimationPlayer.prototype.collectReferencedCells = function() { var steps = this.data.steps || []; var seen = {}; var cells = []; for (var i = 0; i < steps.length; i++) { var step = steps[i]; for (var key in step) { // Skip top-level scalars (wait) and non-cell action keys if (key == 'wait' || key == 'open') { continue; } var sel = step[key]; if (typeof sel === 'object' && sel != null) { var refs = this.graph.getCellsForAction(sel, true); for (var j = 0; j < refs.length; j++) { if (!seen[refs[j].id]) { seen[refs[j].id] = true; cells.push(refs[j]); } } } } } return cells; }; /** * Snapshots the DOM opacity of every cell referenced by any step so * stop() and the next loop iteration can restore it exactly. */ Editor.AnimationPlayer.prototype.snapshotOpacity = function() { var cells = this.collectReferencedCells(); var nodes = this.graph.getNodesForCells(cells); this.snapshot = []; for (var i = 0; i < nodes.length; i++) { this.snapshot.push({ node: nodes[i], opacity: nodes[i].style.opacity }); } }; /** * Restores DOM opacity from the most recent snapshot, clearing any * leftover CSS transitions so the restore is instant. */ Editor.AnimationPlayer.prototype.restoreOpacity = function() { if (this.snapshot == null) return; for (var i = 0; i < this.snapshot.length; i++) { Graph.setTransitionForNodes([this.snapshot[i].node], null); this.snapshot[i].node.style.opacity = this.snapshot[i].opacity; } this.snapshot = null; }; /** * Plays the animation by dispatching its steps as a custom-action chain. * * Options: * loop restart from step 0 on completion * done callback fired when playback ends (not called between loops) */ Editor.AnimationPlayer.prototype.play = function(opts) { if (this.running) { return; } // Don't start on a torn-down graph (e.g. a queued autostart that // fires after the view was disposed). if (this.graph == null || this.graph.isGraphTornDown()) { return; } opts = opts || {}; this.running = true; this.cancelled = false; this.graph.stoppingCustomActions = false; // Make sure cell states exist before we touch their DOM. In // chromeless autoplay, `mxEvent.ROOT` fires synchronously when // the file's root is set — but `view.validate()` runs later, so // at the time the player starts there are no cell states yet // and `getNodesForCells([...])` returns [] for everything. The // first opacity step (legacy converter's implicit "hide all" // prepend) would then no-op, the cells would render at full // opacity, and the user would see the initial visible state // before any fade kicks in. Force validation first so cell // states + shape nodes exist when we snapshot and dispatch. this.graph.view.validate(); var self = this; var steps = this.data.steps || []; // Run steps one batch at a time so progress callbacks // (`opts.onStep`) can fire per batch. A batch is one step plus any // subsequent `immediate: true` steps — they need to dispatch in the // same `executeCustomActions` call for the do/while batching to // pick them up. If we passed only `[steps[idx]]` per call (the // previous behaviour), the next step's `immediate` flag would // never be inspected and the chain would always run sequentially. var runStep = function(idx, done) { // Graph torn down mid-playback — halt without continuing the // chain (don't call done, which could restart the loop). if (self.graph == null || self.graph.isGraphTornDown()) { self.running = false; return; } if (self.cancelled || idx >= steps.length) { done(); return; } // Walk forward to find the end of the immediate batch. // `immediate` on `steps[idx]` itself is ignored (no previous // step to be parallel with) — same semantics as the // `actions[0]` case in `executeCustomActions`. var endIdx = idx + 1; while (endIdx < steps.length && steps[endIdx].immediate === true && !self.cancelled) { endIdx++; } if (typeof opts.onStep == 'function') { // Pass both the start and the exclusive end so the // dialog can highlight the whole parallel batch at // once. Single-step (sequential) calls pass // (idx, idx+1) — same effect as the legacy single-arg // form, which the dialog accepts as a fallback. try { opts.onStep(idx, endIdx); } catch (e) {} } self.graph.executeCustomActions( Graph.flattenAnimationActions(steps.slice(idx, endIdx)), function() { runStep(endIdx, done); }, null); }; var iter = function() { // Stop the loop if the graph was torn down mid-playback — // snapshotOpacity / runStep would otherwise touch a dead view. if (self.graph == null || self.graph.isGraphTornDown()) { self.running = false; return; } if (self.cancelled) { self.restoreOpacity(); self.running = false; return; } self.snapshotOpacity(); runStep(0, function() { if (self.cancelled) { self.restoreOpacity(); self.running = false; return; } // Clear any step highlight (-1 means "no step active") if (typeof opts.onStep == 'function') { try { opts.onStep(-1); } catch (e) {} } if (opts.loop) { // Refresh redraws edges so flow animations restart cleanly self.graph.refresh(); self.restoreOpacity(); window.setTimeout(iter, 0); } else { self.running = false; self.restoreOpacity(); if (opts.done != null) opts.done(); } }); }; // In chromeless autostart, play() runs synchronously from the // mxEvent.ROOT handler that fires inside file.open() — i.e. // BEFORE the lightbox's initial fit (lightboxFit/chromelessResize // run later in EditorUi.fileLoaded). Running the first step now // would let that fit clobber a leading `viewbox`/`scroll` step // (the "first step is skipped" symptom). Deferring the first tick // lets the synchronous fileLoaded flow — including the fit — // finish first. iter() guards on `cancelled`, so a stop() before // the tick fires is safe. if (opts.defer) { window.setTimeout(iter, 0); } else { iter(); } }; /** * Stops playback. The currently-running custom-action chain notices * stoppingCustomActions and unwinds with short-circuited delays; the * iter() callback then runs restoreOpacity. */ Editor.AnimationPlayer.prototype.stop = function() { this.cancelled = true; if (this.graph != null) { this.graph.stoppingCustomActions = true; } // Stop flow animations on every edge we may have touched. Edges keep // their CSS class until explicitly removed — restoreOpacity only // resets opacity, not stroke-dasharray / class. var edges = []; var refs = this.collectReferencedCells(); for (var i = 0; i < refs.length; i++) { if (this.graph.getModel().isEdge(refs[i])) { edges.push(refs[i]); } } if (edges.length > 0) { Editor.toggleFlowAnimation(this.graph, edges, 'stop'); } }; /** * Toggles SVG flow animation on the path elements for the given edges. * The path is given the mxEdgeFlow CSS class which renders a moving * dash via CSS keyframes. */ Editor.toggleFlowAnimation = function(graph, cells, status) { status = status || 'toggle'; for (var i = 0; i < cells.length; i++) { if (!graph.getModel().isEdge(cells[i])) { continue; } var state = graph.view.getState(cells[i]); if (state == null || state.shape == null) { continue; } // Shape-aware path selection (skips the hidden hit/tolerance // paths, finds the rough.js stroke of sketch=1 edges and honors // shape overrides like PipeShape) - the previous hardcoded // paths[1] hit the invisible plain hit path on sketch edges. var path = (state.shape.getFlowAnimationPath != null) ? state.shape.getFlowAnimationPath() : null; if (path == null) { continue; } var hasFlow = path.getAttribute('class') == 'mxEdgeFlow'; var enable = (status == 'start') || (status == 'toggle' && !hasFlow); var disable = (status == 'stop') || (status == 'toggle' && hasFlow); var dashed = mxUtils.getValue(state.style, mxConstants.STYLE_DASHED, '0') == '1'; if (disable) { path.removeAttribute('class'); if (!dashed) { path.removeAttribute('stroke-dasharray'); } } else if (enable) { path.setAttribute('class', 'mxEdgeFlow'); if (!dashed) { path.setAttribute('stroke-dasharray', '8'); } } } }; /** * Plays the animation attached to the given page's model root. The value * can be either the new JSON format ({steps:[...]} or {animation:{...}}) * or the legacy text format — parseAnimationData sniffs and converts. * Returns a new AnimationPlayer, or null if nothing is attached. * * `loop` defaults to whatever the data carries (legacy text animations * and JSON without an explicit `loop` field default to true — the * legacy chromeless animation plugin always looped). Callers can * override either way via `opts.loop`. */ Editor.playAnimationOnGraph = function(graph, opts) { var root = graph.getModel().getRoot(); var raw = null; if (root.value != null && typeof(root.value) == 'object') { raw = root.value.getAttribute('animation'); } if (raw == null || raw == '') { return null; } var data; try { data = Editor.parseAnimationData(raw); } catch (e) { // Malformed JSON in the stored attribute (e.g. hand-edited // XML) — treat as no animation rather than throwing on load. return null; } if (!data.steps || data.steps.length == 0) { return null; } // `enabled: false` is the dialog's "keep the script but don't // autoplay" toggle — equivalent to deleting the steps for runtime // purposes, but preserves them on disk so the user can flip the // toggle back on later without re-authoring. if (data.enabled === false) { return null; } // Default loop from the data unless the caller forced a value. // data.loop is normalized to boolean by parseAnimationData (true // when missing) so this is a simple precedence chain. var playOpts = {}; if (opts != null) { for (var k in opts) playOpts[k] = opts[k]; } if (playOpts.loop == null) { playOpts.loop = data.loop; } // This is the chromeless/file-attached autostart path, which runs // from the mxEvent.ROOT handler before the lightbox's initial fit. // Defer the first step so the fit can't clobber a leading viewbox/ // scroll step (see AnimationPlayer.play). Callers can override. if (playOpts.defer == null) { playOpts.defer = true; } var player = new Editor.AnimationPlayer(graph, data); player.play(playOpts); return player; }; /** * Installs the CSS keyframes required by the flow animation. */ Editor.installAnimationStyles = function() { if (Editor.animationStylesInstalled) { return; } Editor.animationStylesInstalled = true; try { var style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode( '.mxEdgeFlow { animation: mxEdgeFlow 0.5s linear infinite; }' + '@keyframes mxEdgeFlow { to { stroke-dashoffset: -16; } }')); document.getElementsByTagName('head')[0].appendChild(style); } catch (e) { // ignore } }; Editor.installAnimationStyles(); })();