UNPKG

@maxgraph/core

Version:

maxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.

782 lines (778 loc) 30.9 kB
"use strict"; /* Copyright 2021-present The maxGraph project Contributors Copyright (c) 2006-2015, JGraph Ltd Copyright (c) 2006-2015, Gaudenz Alder Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Rectangle_js_1 = __importDefault(require("../view/geometry/Rectangle.js")); const EventObject_js_1 = __importDefault(require("../view/event/EventObject.js")); const EventSource_js_1 = __importDefault(require("../view/event/EventSource.js")); const styleUtils_js_1 = require("../util/styleUtils.js"); const InternalEvent_js_1 = __importDefault(require("../view/event/InternalEvent.js")); const Client_js_1 = __importDefault(require("../Client.js")); const Constants_js_1 = require("../util/Constants.js"); const domUtils_js_1 = require("../util/domUtils.js"); const EventUtils_js_1 = require("../util/EventUtils.js"); let activeWindow = null; /** * Basic window inside a document. * * Creating a simple window. * @example * * ```javascript * var tb = document.createElement('div'); * var wnd = new MaxWindow('Title', tb, 100, 100, 200, 200, true, true); * wnd.setVisible(true); * ``` * * Creating a window that contains an iframe. * @example * * ```javascript * var frame = document.createElement('iframe'); * frame.setAttribute('width', '192px'); * frame.setAttribute('height', '172px'); * frame.setAttribute('src', 'http://www.example.com/'); * frame.style.backgroundColor = 'white'; * * var w = document.body.clientWidth; * var h = (document.body.clientHeight || document.documentElement.clientHeight); * var wnd = new MaxWindow('Title', frame, (w-200)/2, (h-200)/3, 200, 200); * wnd.setVisible(true); * ``` * * To limit the movement of a window, eg. to keep it from being moved beyond * the top, left corner the following method can be overridden (recommended): * * ```javascript * wnd.setLocation(x, y) * { * x = Math.max(0, x); * y = Math.max(0, y); * setLocation.apply(this, arguments); * }; * ``` * * Or the following event handler can be used: * * ```javascript * wnd.addListener(mxEvent.MOVE, function(e) * { * wnd.setLocation(Math.max(0, wnd.getX()), Math.max(0, wnd.getY())); * }); * ``` * * To keep a window inside the current window: * * ```javascript * mxEvent.addListener(window, 'resize', mxUtils.bind(this, function() * { * var iw = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; * var ih = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; * * var x = this.window.getX(); * var y = this.window.getY(); * * if (x + this.window.table.clientWidth > iw) * { * x = Math.max(0, iw - this.window.table.clientWidth); * } * * if (y + this.window.table.clientHeight > ih) * { * y = Math.max(0, ih - this.window.table.clientHeight); * } * * if (this.window.getX() != x || this.window.getY() != y) * { * this.window.setLocation(x, y); * } * })); * ``` * * ### Event: mxEvent.MOVE_START * * Fires before the window is moved. The <code>event</code> property contains * the corresponding mouse event. * * ### Event: mxEvent.MOVE * * Fires while the window is being moved. The <code>event</code> property * contains the corresponding mouse event. * * ### Event: mxEvent.MOVE_END * * Fires after the window is moved. The <code>event</code> property contains * the corresponding mouse event. * * ### Event: mxEvent.RESIZE_START * * Fires before the window is resized. The <code>event</code> property contains * the corresponding mouse event. * * ### Event: mxEvent.RESIZE * * Fires while the window is being resized. The <code>event</code> property * contains the corresponding mouse event. * * ### Event: mxEvent.RESIZE_END * * Fires after the window is resized. The <code>event</code> property contains * the corresponding mouse event. * * ### Event: mxEvent.MAXIMIZE * * Fires after the window is maximized. The <code>event</code> property * contains the corresponding mouse event. * * ### Event: mxEvent.MINIMIZE * * Fires after the window is minimized. The <code>event</code> property * contains the corresponding mouse event. * * ### Event: mxEvent.NORMALIZE * * Fires after the window is normalized, that is, it returned from * maximized or minimized state. The <code>event</code> property contains the * corresponding mouse event. * * ### Event: mxEvent.ACTIVATE * * Fires after a window is activated. The <code>previousWindow</code> property * contains the previous window. The event sender is the active window. * * ### Event: mxEvent.SHOW * * Fires after the window is shown. This event has no properties. * * ### Event: mxEvent.HIDE * * Fires after the window is hidden. This event has no properties. * * ### Event: mxEvent.CLOSE * * Fires before the window is closed. The <code>event</code> property contains * the corresponding mouse event. * * ### Event: mxEvent.DESTROY * * Fires before the window is destroyed. This event has no properties. * * @category GUI */ class MaxWindow extends EventSource_js_1.default { constructor(title, content, x, y, width = null, height = null, minimizable = true, movable = true, replaceNode = null, style) { super(); /** * URL of the image to be used for the close icon in the titlebar. */ this.closeImage = `${Client_js_1.default.imageBasePath}/close.gif`; /** * URL of the image to be used for the minimize icon in the titlebar. */ this.minimizeImage = `${Client_js_1.default.imageBasePath}/minimize.gif`; /** * URL of the image to be used for the normalize icon in the titlebar. */ this.normalizeImage = `${Client_js_1.default.imageBasePath}/normalize.gif`; /** * URL of the image to be used for the maximize icon in the titlebar. */ this.maximizeImage = `${Client_js_1.default.imageBasePath}/maximize.gif`; /** * URL of the image to be used for the resize icon. */ this.resizeImage = `${Client_js_1.default.imageBasePath}/resize.gif`; /** * Boolean flag that represents the visible state of the window. */ this.visible = false; /** * {@link Rectangle} that specifies the minimum width and height of the window. * Default is (50, 40). */ this.minimumSize = new Rectangle_js_1.default(0, 0, 50, 40); /** * Specifies if the window should be destroyed when it is closed. If this * is false then the window is hidden using <setVisible>. Default is true. */ this.destroyOnClose = true; if (content != null) { this.content = content; this.init(x, y, width, height, style); this.installMaximizeHandler(); this.installMinimizeHandler(); this.installCloseHandler(); this.setMinimizable(minimizable); this.setTitle(title); if (movable) { this.installMoveHandler(); } if (replaceNode != null && replaceNode.parentNode != null) { replaceNode.parentNode.replaceChild(this.div, replaceNode); } else { document.body.appendChild(this.div); } } } /** * Initializes the DOM tree that represents the window. */ init(x, y, width = null, height = null, style = 'mxWindow') { this.div = document.createElement('div'); this.div.className = style; this.div.style.left = `${x}px`; this.div.style.top = `${y}px`; this.table = document.createElement('table'); this.table.className = style; // Disables built-in pan and zoom in IE10 and later if (Client_js_1.default.IS_POINTER) { this.div.style.touchAction = 'none'; } // Workaround for table size problems in FF if (width != null) { this.div.style.width = `${width}px`; this.table.style.width = `${width}px`; } if (height != null) { this.div.style.height = `${height}px`; this.table.style.height = `${height}px`; } // Creates title row const tbody = document.createElement('tbody'); let tr = document.createElement('tr'); this.title = document.createElement('td'); this.title.className = `${style}Title`; this.buttons = document.createElement('div'); this.buttons.style.position = 'absolute'; this.buttons.style.display = 'inline-block'; this.buttons.style.right = '4px'; this.buttons.style.top = '5px'; this.title.appendChild(this.buttons); tr.appendChild(this.title); tbody.appendChild(tr); // Creates content row and table cell tr = document.createElement('tr'); this.td = document.createElement('td'); this.td.className = `${style}Pane`; this.contentWrapper = document.createElement('div'); this.contentWrapper.className = `${style}Pane`; this.contentWrapper.style.width = '100%'; this.contentWrapper.appendChild(this.content); // Workaround for div around div restricts height // of inner div if outerdiv has hidden overflow if (this.content.nodeName.toUpperCase() !== 'DIV') { this.contentWrapper.style.height = '100%'; } // Puts all content into the DOM this.td.appendChild(this.contentWrapper); tr.appendChild(this.td); tbody.appendChild(tr); this.table.appendChild(tbody); this.div.appendChild(this.table); // Puts the window on top of other windows when clicked const activator = (evt) => { this.activate(); }; InternalEvent_js_1.default.addGestureListeners(this.title, activator); InternalEvent_js_1.default.addGestureListeners(this.table, activator); this.hide(); } /** * Sets the window title to the given string. HTML markup inside the title * will be escaped. */ setTitle(title) { // Removes all text content nodes (normally just one) let child = this.title.firstChild; while (child != null) { const next = child.nextSibling; if (child.nodeType === Constants_js_1.NODE_TYPE.TEXT) { child.parentNode.removeChild(child); } child = next; } (0, domUtils_js_1.write)(this.title, title || ''); this.title.appendChild(this.buttons); } /** * Sets if the window contents should be scrollable. */ setScrollable(scrollable) { // Workaround for hang in Presto 2.5.22 (Opera 10.5) if (navigator.userAgent == null || navigator.userAgent.indexOf('Presto/2.5') < 0) { if (scrollable) { this.contentWrapper.style.overflow = 'auto'; } else { this.contentWrapper.style.overflow = 'hidden'; } } } /** * Puts the window on top of all other windows. */ activate() { if (activeWindow !== this) { const style = (0, styleUtils_js_1.getCurrentStyle)(this.getElement()); const index = style != null ? parseInt(style.zIndex) : 3; if (activeWindow) { const elt = activeWindow.getElement(); if (elt?.style) { elt.style.zIndex = String(index); } } const previousWindow = activeWindow; this.getElement().style.zIndex = String(index + 1); // eslint-disable-next-line @typescript-eslint/no-this-alias -- we need to maintain the reference to the current window activeWindow = this; this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.ACTIVATE, { previousWindow })); } } /** * Returuns the outermost DOM node that makes up the window. */ getElement() { return this.div; } /** * Makes sure the window is inside the client area of the window. */ fit() { (0, styleUtils_js_1.fit)(this.div); } /** * Returns true if the window is resizable. */ isResizable() { if (this.resize != null) { return this.resize.style.display !== 'none'; } return false; } /** * Sets if the window should be resizable. To avoid interference with some * built-in features of IE10 and later, the use of the following code is * recommended if there are resizable <MaxWindow>s in the page: * * ```javascript * if (Client.IS_POINTER) * { * document.body.style.msTouchAction = 'none'; * } * ``` */ setResizable(resizable) { if (resizable) { if (this.resize == null) { this.resize = document.createElement('img'); this.resize.style.position = 'absolute'; this.resize.style.bottom = '2px'; this.resize.style.right = '2px'; this.resize.setAttribute('src', this.resizeImage); this.resize.style.cursor = 'nw-resize'; let startX = null; let startY = null; let width = null; let height = null; const start = (evt) => { // LATER: pointerdown starting on border of resize does start // the drag operation but does not fire consecutive events via // one of the listeners below (does pan instead). // Workaround: document.body.style.msTouchAction = 'none' this.activate(); startX = (0, EventUtils_js_1.getClientX)(evt); startY = (0, EventUtils_js_1.getClientY)(evt); width = this.div.offsetWidth; height = this.div.offsetHeight; InternalEvent_js_1.default.addGestureListeners(document, null, dragHandler, dropHandler); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.RESIZE_START, { event: evt })); InternalEvent_js_1.default.consume(evt); }; // Adds a temporary pair of listeners to intercept // the gesture event in the document const dragHandler = (evt) => { if (startX != null && startY != null) { const dx = (0, EventUtils_js_1.getClientX)(evt) - startX; const dy = (0, EventUtils_js_1.getClientY)(evt) - startY; if (width != null && height != null) { this.setSize(width + dx, height + dy); } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.RESIZE, { event: evt })); InternalEvent_js_1.default.consume(evt); } }; const dropHandler = (evt) => { if (startX != null && startY != null) { startX = null; startY = null; InternalEvent_js_1.default.removeGestureListeners(document, null, dragHandler, dropHandler); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.RESIZE_END, { event: evt })); InternalEvent_js_1.default.consume(evt); } }; InternalEvent_js_1.default.addGestureListeners(this.resize, start, dragHandler, dropHandler); this.div.appendChild(this.resize); } else { this.resize.style.display = 'inline'; } } else if (this.resize != null) { this.resize.style.display = 'none'; } } /** * Sets the size of the window. */ setSize(width, height) { width = Math.max(this.minimumSize.width, width); height = Math.max(this.minimumSize.height, height); // Workaround for table size problems in FF this.div.style.width = `${width}px`; this.div.style.height = `${height}px`; this.table.style.width = `${width}px`; this.table.style.height = `${height}px`; this.contentWrapper.style.height = `${this.div.offsetHeight - this.title.offsetHeight}px`; } /** * Sets if the window is minimizable. */ setMinimizable(minimizable) { this.minimize.style.display = minimizable ? '' : 'none'; } /** * Returns an {@link Rectangle} that specifies the size for the minimized window. * A width or height of 0 means keep the existing width or height. This * implementation returns the height of the window title and keeps the width. */ getMinimumSize() { return new Rectangle_js_1.default(0, 0, 0, this.title.offsetHeight); } /** * Installs the event listeners required for minimizing the window. */ installMinimizeHandler() { this.minimize = document.createElement('img'); this.minimize.setAttribute('src', this.minimizeImage); this.minimize.setAttribute('title', 'Minimize'); this.minimize.style.cursor = 'pointer'; this.minimize.style.marginLeft = '2px'; this.minimize.style.display = 'none'; this.buttons.appendChild(this.minimize); let minimized = false; let maxDisplay = null; let height = null; const funct = (evt) => { this.activate(); if (!minimized) { minimized = true; this.minimize.setAttribute('src', this.normalizeImage); this.minimize.setAttribute('title', 'Normalize'); this.contentWrapper.style.display = 'none'; maxDisplay = this.maximize.style.display; this.maximize.style.display = 'none'; height = this.table.style.height; const minSize = this.getMinimumSize(); if (minSize.height > 0) { this.div.style.height = `${minSize.height}px`; this.table.style.height = `${minSize.height}px`; } if (minSize.width > 0) { this.div.style.width = `${minSize.width}px`; this.table.style.width = `${minSize.width}px`; } if (this.resize != null) { this.resize.style.visibility = 'hidden'; } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.MINIMIZE, { event: evt })); } else { minimized = false; this.minimize.setAttribute('src', this.minimizeImage); this.minimize.setAttribute('title', 'Minimize'); this.contentWrapper.style.display = ''; // default if (maxDisplay != null && height != null) { this.maximize.style.display = maxDisplay; this.div.style.height = height; this.table.style.height = height; } if (this.resize != null) { this.resize.style.visibility = ''; } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.NORMALIZE, { event: evt })); } InternalEvent_js_1.default.consume(evt); }; InternalEvent_js_1.default.addGestureListeners(this.minimize, funct); } /** * Sets if the window is maximizable. */ setMaximizable(maximizable) { this.maximize.style.display = maximizable ? '' : 'none'; } /** * Installs the event listeners required for maximizing the window. */ installMaximizeHandler() { this.maximize = document.createElement('img'); this.maximize.setAttribute('src', this.maximizeImage); this.maximize.setAttribute('title', 'Maximize'); this.maximize.style.cursor = 'default'; this.maximize.style.marginLeft = '2px'; this.maximize.style.cursor = 'pointer'; this.maximize.style.display = 'none'; this.buttons.appendChild(this.maximize); let maximized = false; let x = null; let y = null; let height = null; let width = null; let minDisplay = null; const funct = (evt) => { this.activate(); if (this.maximize.style.display !== 'none') { if (!maximized) { maximized = true; this.maximize.setAttribute('src', this.normalizeImage); this.maximize.setAttribute('title', 'Normalize'); this.contentWrapper.style.display = ''; minDisplay = this.minimize.style.display; this.minimize.style.display = 'none'; // Saves window state x = parseInt(this.div.style.left); y = parseInt(this.div.style.top); height = this.table.style.height; width = this.table.style.width; this.div.style.left = '0px'; this.div.style.top = '0px'; const docHeight = Math.max(document.body.clientHeight || 0, document.documentElement.clientHeight || 0); this.div.style.width = `${document.body.clientWidth - 2}px`; this.div.style.height = `${docHeight - 2}px`; this.table.style.width = `${document.body.clientWidth - 2}px`; this.table.style.height = `${docHeight - 2}px`; if (this.resize != null) { this.resize.style.visibility = 'hidden'; } const style = (0, styleUtils_js_1.getCurrentStyle)(this.contentWrapper); if (style.overflow === 'auto' || this.resize != null) { this.contentWrapper.style.height = `${this.div.offsetHeight - this.title.offsetHeight}px`; } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.MAXIMIZE, { event: evt })); } else { maximized = false; this.maximize.setAttribute('src', this.maximizeImage); this.maximize.setAttribute('title', 'Maximize'); this.contentWrapper.style.display = ''; if (minDisplay != null) { this.minimize.style.display = minDisplay; } // Restores window state this.div.style.left = `${x}px`; this.div.style.top = `${y}px`; if (width != null && height != null) { this.div.style.height = height; this.div.style.width = width; } const style = (0, styleUtils_js_1.getCurrentStyle)(this.contentWrapper); if (style.overflow === 'auto' || this.resize != null) { this.contentWrapper.style.height = `${this.div.offsetHeight - this.title.offsetHeight}px`; } if (width != null && height != null) { this.table.style.height = height; this.table.style.width = width; } if (this.resize != null) { this.resize.style.visibility = ''; } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.NORMALIZE, { event: evt })); } InternalEvent_js_1.default.consume(evt); } }; InternalEvent_js_1.default.addGestureListeners(this.maximize, funct); InternalEvent_js_1.default.addListener(this.title, 'dblclick', funct); } /** * Installs the event listeners required for moving the window. */ installMoveHandler() { this.title.style.cursor = 'move'; InternalEvent_js_1.default.addGestureListeners(this.title, (evt) => { const startX = (0, EventUtils_js_1.getClientX)(evt); const startY = (0, EventUtils_js_1.getClientY)(evt); const x = this.getX(); const y = this.getY(); // Adds a temporary pair of listeners to intercept // the gesture event in the document const dragHandler = (evt) => { const dx = (0, EventUtils_js_1.getClientX)(evt) - startX; const dy = (0, EventUtils_js_1.getClientY)(evt) - startY; this.setLocation(x + dx, y + dy); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.MOVE, { event: evt })); InternalEvent_js_1.default.consume(evt); }; const dropHandler = (evt) => { InternalEvent_js_1.default.removeGestureListeners(document, null, dragHandler, dropHandler); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.MOVE_END, { event: evt })); InternalEvent_js_1.default.consume(evt); }; InternalEvent_js_1.default.addGestureListeners(document, null, dragHandler, dropHandler); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.MOVE_START, { event: evt })); InternalEvent_js_1.default.consume(evt); }); // Disables built-in pan and zoom in IE10 and later if (Client_js_1.default.IS_POINTER) { this.title.style.touchAction = 'none'; } } /** * Sets the upper, left corner of the window. */ setLocation(x, y) { this.div.style.left = `${x}px`; this.div.style.top = `${y}px`; } /** * Returns the current position on the x-axis. */ getX() { return parseInt(this.div.style.left); } /** * Returns the current position on the y-axis. */ getY() { return parseInt(this.div.style.top); } /** * Adds the <closeImage> as a new image node in <closeImg> and installs the * <close> event. */ installCloseHandler() { this.closeImg = document.createElement('img'); this.closeImg.setAttribute('src', this.closeImage); this.closeImg.setAttribute('title', 'Close'); this.closeImg.style.marginLeft = '2px'; this.closeImg.style.cursor = 'pointer'; this.closeImg.style.display = 'none'; this.buttons.appendChild(this.closeImg); InternalEvent_js_1.default.addGestureListeners(this.closeImg, (evt) => { this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.CLOSE, { event: evt })); if (this.destroyOnClose) { this.destroy(); } else { this.setVisible(false); } InternalEvent_js_1.default.consume(evt); }); } /** * Sets the image associated with the window. * * * @param image - URL of the image to be used. */ setImage(image) { this.image = document.createElement('img'); this.image.setAttribute('src', image); this.image.setAttribute('align', 'left'); this.image.style.marginRight = '4px'; this.image.style.marginLeft = '0px'; this.image.style.marginTop = '-2px'; this.title.insertBefore(this.image, this.title.firstChild); } /** * Sets the image associated with the window. * * * @param closable - Boolean specifying if the window should be closable. */ setClosable(closable) { this.closeImg.style.display = closable ? '' : 'none'; } /** * Returns true if the window is visible. */ isVisible() { if (this.div != null) { return this.div.style.display !== 'none'; } return false; } /** * Shows or hides the window depending on the given flag. * * * @param visible - Boolean indicating if the window should be made visible. */ setVisible(visible) { if (this.div != null && this.isVisible() !== visible) { if (visible) { this.show(); } else { this.hide(); } } } /** * Shows the window. */ show() { this.div.style.display = ''; this.activate(); const style = (0, styleUtils_js_1.getCurrentStyle)(this.contentWrapper); if ((style.overflow == 'auto' || this.resize != null) && this.contentWrapper.style.display != 'none') { this.contentWrapper.style.height = `${this.div.offsetHeight - this.title.offsetHeight}px`; } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.SHOW)); } /** * Hides the window. */ hide() { this.div.style.display = 'none'; this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.HIDE)); } /** * Destroys the window and removes all associated resources. Fires a * <destroy> event prior to destroying the window. */ destroy() { this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.DESTROY)); if (this.div != null) { InternalEvent_js_1.default.release(this.div); // @ts-ignore this.div.parentNode.removeChild(this.div); // @ts-ignore this.div = null; } // @ts-ignore this.title = null; // @ts-ignore this.content = null; // @ts-ignore this.contentWrapper = null; } } exports.default = MaxWindow;