UNPKG

@maxgraph/core

Version:

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

457 lines (453 loc) 17.3 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 }); exports.getAlignmentAsPoint = exports.sortCells = exports.getSizeForString = exports.setOpacity = exports.setStyleFlag = exports.setCellStyleFlags = exports.setCellStyles = exports.convertPoint = exports.getScrollOrigin = exports.getDocumentScrollOrigin = exports.getOffset = exports.fit = exports.getDocumentSize = exports.hasScrollbars = exports.setPrefixedStyle = exports.parseCssNumber = exports.getCurrentStyle = exports.removeCursors = void 0; const Client_js_1 = __importDefault(require("../Client.js")); const Constants_js_1 = require("./Constants.js"); const Point_js_1 = __importDefault(require("../view/geometry/Point.js")); const CellPath_js_1 = __importDefault(require("../view/cell/CellPath.js")); const Rectangle_js_1 = __importDefault(require("../view/geometry/Rectangle.js")); const utils_js_1 = require("../internal/utils.js"); /** * Removes the cursors from the style of the given DOM node and its descendants. * * @param element DOM node to remove the cursor style from. */ const removeCursors = (element) => { if (element.style) { element.style.cursor = ''; } const children = element.children; if (children) { const childCount = children.length; for (let i = 0; i < childCount; i += 1) { (0, exports.removeCursors)(children[i]); } } }; exports.removeCursors = removeCursors; /** * Returns the current style of the specified element. * * @param element DOM node whose current style should be returned. */ const getCurrentStyle = (element) => { return !element || element.toString() === '[object ShadowRoot]' ? null : window.getComputedStyle(element, ''); }; exports.getCurrentStyle = getCurrentStyle; /** * Parses the given CSS numeric value adding handling for the values thin, medium and thick (2, 4 and 6). */ const parseCssNumber = (value) => { if (value === 'thin') { value = '2'; } else if (value === 'medium') { value = '4'; } else if (value === 'thick') { value = '6'; } let n = parseFloat(value); if (Number.isNaN(n)) { n = 0; } return n; }; exports.parseCssNumber = parseCssNumber; /** * Adds the given style with the standard name and an optional vendor prefix for the current browser. * * ```javascript * styleUtils.setPrefixedStyle(node.style, 'transformOrigin', '0% 0%'); * ``` */ const setPrefixedStyle = (style, name, value) => { let prefix = null; if (Client_js_1.default.IS_SF || Client_js_1.default.IS_GC) { prefix = 'Webkit'; } else if (Client_js_1.default.IS_MT) { prefix = 'Moz'; } style.setProperty(name, value); if (prefix !== null && name.length > 0) { name = prefix + name.substring(0, 1).toUpperCase() + name.substring(1); style.setProperty(name, value); } }; exports.setPrefixedStyle = setPrefixedStyle; /** * Function: hasScrollbars * * Returns true if the overflow CSS property of the given node is either * scroll or auto. * * @param node DOM node whose style should be checked for scrollbars. */ const hasScrollbars = (node) => { const style = (0, exports.getCurrentStyle)(node); return !!style && (style.overflow === 'scroll' || style.overflow === 'auto'); }; exports.hasScrollbars = hasScrollbars; /** * Returns the client size for the current document as an {@link Rectangle}. */ const getDocumentSize = () => { const b = document.body; const d = document.documentElement; try { return new Rectangle_js_1.default(0, 0, b.clientWidth ?? d.clientWidth, Math.max(b.clientHeight ?? 0, d.clientHeight)); } catch (e) { return new Rectangle_js_1.default(); } }; exports.getDocumentSize = getDocumentSize; /** * Makes sure the given node is inside the visible area of the window. * This is done by setting the left and top in the style. */ const fit = (node) => { const ds = (0, exports.getDocumentSize)(); const left = node.offsetLeft; const width = node.offsetWidth; const offset = (0, exports.getDocumentScrollOrigin)(node.ownerDocument); const sl = offset.x; const st = offset.y; const right = sl + ds.width; if (left + width > right) { node.style.left = `${Math.max(sl, right - width)}px`; } const top = node.offsetTop; const height = node.offsetHeight; const bottom = st + ds.height; if (top + height > bottom) { node.style.top = `${Math.max(st, bottom - height)}px`; } }; exports.fit = fit; /** * Returns the offset for the specified container as an {@link Point}. * The offset is the distance from the top left corner of the container to the top left corner of the document. * * @param container DOM node to return the offset for. * @param scrollOffset Optional boolean to add the scroll offset of the document. Default is `false`. */ const getOffset = (container, scrollOffset = false) => { let offsetLeft = 0; let offsetTop = 0; // Ignores document scroll origin for fixed elements let fixed = false; let node = container; const b = document.body; const d = document.documentElement; while (node != null && node != b && node != d && !fixed) { const style = (0, exports.getCurrentStyle)(node); if (style != null) { fixed = fixed || style.position == 'fixed'; } node = node.parentNode; } if (!scrollOffset && !fixed) { const offset = (0, exports.getDocumentScrollOrigin)(container.ownerDocument); offsetLeft += offset.x; offsetTop += offset.y; } const r = container.getBoundingClientRect(); if (r != null) { offsetLeft += r.left; offsetTop += r.top; } return new Point_js_1.default(offsetLeft, offsetTop); }; exports.getOffset = getOffset; /** * Returns the scroll origin of the given document or the current document if no document is given. */ const getDocumentScrollOrigin = (doc) => { // @ts-ignore 'parentWindow' is an unknown property. const wnd = doc.defaultView || doc.parentWindow; const x = wnd != null && window.pageXOffset !== undefined ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body) .scrollLeft; const y = wnd != null && window.pageYOffset !== undefined ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop; return new Point_js_1.default(x, y); }; exports.getDocumentScrollOrigin = getDocumentScrollOrigin; /** * Returns the top, left corner of the view rectangle as an {@link Point}. * * @param node DOM node whose scroll origin should be returned. * @param includeAncestors Whether the scroll origin of the ancestors should be included. Default is `false`. * @param includeDocument Whether the scroll origin of the document should be included. Default is `true`. */ const getScrollOrigin = (node = null, includeAncestors = false, includeDocument = true) => { const doc = node != null ? node.ownerDocument : document; const b = doc.body; const d = doc.documentElement; const result = new Point_js_1.default(); let fixed = false; while (node != null && node != b && node != d) { if (!Number.isNaN(node.scrollLeft) && !Number.isNaN(node.scrollTop)) { result.x += node.scrollLeft; result.y += node.scrollTop; } const style = (0, exports.getCurrentStyle)(node); if (style != null) { fixed = fixed || style.position == 'fixed'; } node = includeAncestors ? node.parentNode : null; } if (!fixed && includeDocument) { const origin = (0, exports.getDocumentScrollOrigin)(doc); result.x += origin.x; result.y += origin.y; } return result; }; exports.getScrollOrigin = getScrollOrigin; /** * Converts the specified point (x, y) using the offset of the specified container and returns a new {@link Point} with the result. * * ```javascript * const pt = styleUtils.convertPoint(graph.container, eventUtils.getClientX(evt), eventUtils.getClientY(evt)); * ``` * * @param container DOM node to use for the offset. * @param x X-coordinate of the point to be converted. * @param y Y-coordinate of the point to be converted. */ const convertPoint = (container, x, y) => { const origin = (0, exports.getScrollOrigin)(container, false); const offset = (0, exports.getOffset)(container); offset.x -= origin.x; offset.y -= origin.y; return new Point_js_1.default(x - offset.x, y - offset.y); }; exports.convertPoint = convertPoint; /** * Assigns the value for the given key in the styles of the given cells, or removes the key from the styles if the value is `null`. * * @param model {@link GraphDataModel} to execute the transaction in. * @param cells Array of {@link Cell}s to be updated. * @param key Key of the style to be changed. * @param value New value for the given key. */ const setCellStyles = (model, cells, key, value) => { if (cells.length > 0) { model.batchUpdate(() => { for (let i = 0; i < cells.length; i += 1) { const cell = cells[i]; if (cell) { // Currently, the style object must be cloned, otherwise model.setStyle does not trigger the change event and the cell state in the view is not updated const style = cell.getClonedStyle(); style[key] = value; model.setStyle(cell, style); } } }); } }; exports.setCellStyles = setCellStyles; /** * Sets or toggles the flag bit for the given key in the cell's styles. * If the `value` parameter is not set, then the flag is toggled. * * Example that toggles the bold font style: * * ```javascript * const cells = graph.getSelectionCells(); * setCellStyleFlags(graph.model, * cells, * 'fontStyle', * constants.FONT_STYLE_FLAG.BOLD); * ``` * * @param model {@link GraphDataModel} that contains the cells. * @param cells Array of {@link Cell}s to change the style for. * @param key Key of the style to be changed. * @param flag Integer for the bit to be changed. * @param value Optional boolean value for the flag. */ const setCellStyleFlags = (model, cells, key, flag, value) => { if (cells.length > 0) { model.batchUpdate(() => { for (let i = 0; i < cells.length; i += 1) { const cell = cells[i]; if (cell) { // Currently, the style object must be cloned, otherwise model.setStyle does not trigger the change event and the cell state in the view is not updated const style = (0, exports.setStyleFlag)(cell.getClonedStyle(), key, flag, value); model.setStyle(cell, style); } } }); } }; exports.setCellStyleFlags = setCellStyleFlags; /** * Sets or toggles the flag bit for the given key in the cell's style. * If the `value` parameter is not set, then the flag is toggled. * * @param style The style of the Cell. * @param key Key of the style to be changed. * @param flag Integer for the bit to be changed. * @param value Optional boolean value for the given flag. */ const setStyleFlag = (style, key, flag, value) => { const v = style[key]; if (v === undefined) { style[key] = value === undefined || value ? flag : 0; } else { if (value === undefined) { style[key] = v ^ flag; } else if (value) { style[key] = v | flag; } else { style[key] = v & ~flag; } } return style; }; exports.setStyleFlag = setStyleFlag; /** * Sets the opacity of the specified DOM node to the given value in %. * * @param node DOM node to set the opacity for. * @param value Opacity in %. Possible values are between 0 and 100. */ const setOpacity = (node, value) => { node.style.opacity = String(value / 100); }; exports.setOpacity = setOpacity; /** * Returns an {@link Rectangle} with the size (width and height in pixels) of the given string. * The string may contain HTML markup. * Newlines should be converted to `<br>` before calling this method. * The caller is responsible for sanitizing the HTML markup. * * Example: * * ```javascript * const label = graph.getLabel(cell).replace(/\n/g, "<br>"); * const size = graph.getSizeForString(label); * ``` * * @param text String whose size should be returned. * @param fontSize Integer that specifies the font size in pixels. Default is {@link DEFAULT_FONTSIZE}. * @param fontFamily String that specifies the name of the font family. Default is {@link DEFAULT_FONTFAMILY}. * @param textWidth Optional width for text wrapping. * @param fontStyle Optional font style, value generally taken from {@link CellStateStyle.fontStyle}. */ const getSizeForString = (text, fontSize = Constants_js_1.DEFAULT_FONTSIZE, fontFamily = Constants_js_1.DEFAULT_FONTFAMILY, textWidth = null, fontStyle = null) => { const div = document.createElement('div'); // Sets the font size and family div.style.fontFamily = fontFamily; div.style.fontSize = `${Math.round(fontSize)}px`; div.style.lineHeight = `${Math.round(fontSize * Constants_js_1.LINE_HEIGHT)}px`; // Sets the font style if (fontStyle !== null) { (0, utils_js_1.matchBinaryMask)(fontStyle, Constants_js_1.FONT_STYLE_MASK.BOLD) && (div.style.fontWeight = 'bold'); (0, utils_js_1.matchBinaryMask)(fontStyle, Constants_js_1.FONT_STYLE_MASK.ITALIC) && (div.style.fontStyle = 'italic'); const txtDecor = []; (0, utils_js_1.matchBinaryMask)(fontStyle, Constants_js_1.FONT_STYLE_MASK.UNDERLINE) && txtDecor.push('underline'); (0, utils_js_1.matchBinaryMask)(fontStyle, Constants_js_1.FONT_STYLE_MASK.STRIKETHROUGH) && txtDecor.push('line-through'); txtDecor.length > 0 && (div.style.textDecoration = txtDecor.join(' ')); } // Disables block layout and outside wrapping and hides the div div.style.position = 'absolute'; div.style.visibility = 'hidden'; div.style.display = 'inline-block'; if (textWidth !== null) { div.style.width = `${textWidth}px`; div.style.whiteSpace = 'normal'; } else { div.style.whiteSpace = 'nowrap'; } // Adds the text and inserts into DOM for updating of size div.innerHTML = text; document.body.appendChild(div); // Gets the size and removes from DOM const size = new Rectangle_js_1.default(0, 0, div.offsetWidth, div.offsetHeight); document.body.removeChild(div); return size; }; exports.getSizeForString = getSizeForString; /** * Sorts the given cells according to the order in the cell hierarchy. * Ascending is optional and defaults to `true`. */ const sortCells = (cells, ascending = true) => { const lookup = new Map(); cells.sort((o1, o2) => { let p1 = lookup.get(o1); if (p1 == null) { p1 = CellPath_js_1.default.create(o1).split(CellPath_js_1.default.PATH_SEPARATOR); lookup.set(o1, p1); } let p2 = lookup.get(o2); if (p2 == null) { p2 = CellPath_js_1.default.create(o2).split(CellPath_js_1.default.PATH_SEPARATOR); lookup.set(o2, p2); } const comp = CellPath_js_1.default.compare(p1, p2); return comp == 0 ? 0 : comp > 0 == ascending ? 1 : -1; }); return cells; }; exports.sortCells = sortCells; /** * Returns an {@link Point} that represents the horizontal and vertical alignment for numeric computations. * * X is -0.5 for center, -1 for right and 0 for left alignment. * Y is -0.5 for middle, -1 for bottom and 0 for top alignment. * * Default values for missing arguments is center and middle. */ const getAlignmentAsPoint = (align, valign) => { let dx = -0.5; let dy = -0.5; // Horizontal alignment if (align === 'left') { dx = 0; } else if (align === 'right') { dx = -1; } // Vertical alignment if (valign === 'top') { dy = 0; } else if (valign === 'bottom') { dy = -1; } return new Point_js_1.default(dx, dy); }; exports.getAlignmentAsPoint = getAlignmentAsPoint;