converting to typescript

development
mcyph 2021-04-08 20:36:21 +10:00
parent 16060a58bd
commit 889156b314
17 changed files with 1658 additions and 1983 deletions

View File

@ -14,6 +14,136 @@ import mxRectangle from '../util/datatypes/mxRectangle';
import mxCellState from '../util/datatypes/mxCellState'; import mxCellState from '../util/datatypes/mxCellState';
class mxText extends mxShape { class mxText extends mxShape {
/**
* Class: mxText
*
* Extends <mxShape> to implement a text shape. To change vertical text from
* bottom to top to top to bottom, the following code can be used:
*
* (code)
* verticalTextRotation = 90;
* (end)
*
* Constructor: mxText
*
* Constructs a new text shape.
*
* Parameters:
*
* value - String that represents the text to be displayed. This is stored in
* <value>.
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* align - Specifies the horizontal alignment. Default is ''. This is stored in
* <align>.
* valign - Specifies the vertical alignment. Default is ''. This is stored in
* <valign>.
* color - String that specifies the text color. Default is 'black'. This is
* stored in <color>.
* family - String that specifies the font family. Default is
* <mxConstants.DEFAULT_FONTFAMILY>. This is stored in <family>.
* size - Integer that specifies the font size. Default is
* <mxConstants.DEFAULT_FONTSIZE>. This is stored in <size>.
* fontStyle - Specifies the font style. Default is 0. This is stored in
* <fontStyle>.
* spacing - Integer that specifies the global spacing. Default is 2. This is
* stored in <spacing>.
* spacingTop - Integer that specifies the top spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingTop>.
* spacingRight - Integer that specifies the right spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingRight>.
* spacingBottom - Integer that specifies the bottom spacing. Default is 0.The
* sum of the spacing and this is stored in <spacingBottom>.
* spacingLeft - Integer that specifies the left spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingLeft>.
* horizontal - Boolean that specifies if the label is horizontal. Default is
* true. This is stored in <horizontal>.
* background - String that specifies the background color. Default is null.
* This is stored in <background>.
* border - String that specifies the label border color. Default is null.
* This is stored in <border>.
* wrap - Specifies if word-wrapping should be enabled. Default is false.
* This is stored in <wrap>.
* clipped - Specifies if the label should be clipped. Default is false.
* This is stored in <clipped>.
* overflow - Value of the overflow style. Default is 'visible'.
*/
constructor(
value: string,
bounds: mxRectangle,
align: string=mxConstants.ALIGN_CENTER,
valign: string=mxConstants.ALIGN_MIDDLE,
color: string='black',
family: string=mxConstants.DEFAULT_FONTFAMILY,
size: number=mxConstants.DEFAULT_FONTSIZE,
fontStyle: number=mxConstants.DEFAULT_FONTSTYLE,
spacing: number=2,
spacingTop: number=0,
spacingRight: number=0,
spacingBottom: number=0,
spacingLeft: number=0,
horizontal: boolean=true,
background: string | null=null,
border: string | null=null,
wrap: boolean=false,
clipped: boolean=false,
overflow: string='visible',
labelPadding: number=0,
textDirection
) {
super();
this.value = value;
this.bounds = bounds;
this.color = color;
this.align = align;
this.valign = valign;
this.family = family;
this.size = size;
this.fontStyle = fontStyle;
this.spacing = parseInt(String(spacing || 2));
this.spacingTop = parseInt(String(spacing || 2)) + parseInt(String(spacingTop || 0));
this.spacingRight = parseInt(String(spacing || 2)) + parseInt(String(spacingRight || 0));
this.spacingBottom = parseInt(String(spacing || 2)) + parseInt(String(spacingBottom || 0));
this.spacingLeft = parseInt(String(spacing || 2)) + parseInt(String(spacingLeft || 0));
this.horizontal = horizontal;
this.background = background;
this.border = border;
this.wrap = wrap;
this.clipped = clipped;
this.overflow = overflow;
this.labelPadding = labelPadding;
this.textDirection = textDirection;
this.rotation = 0;
this.updateMargin();
}
// TODO: Document me!
value: string;
bounds: mxRectangle;
align: string=mxConstants.ALIGN_CENTER;
valign: string=mxConstants.ALIGN_MIDDLE;
color: string='black';
family: string=mxConstants.DEFAULT_FONTFAMILY;
size: number=mxConstants.DEFAULT_FONTSIZE;
fontStyle: number=mxConstants.DEFAULT_FONTSTYLE;
spacing: number=2;
spacingTop: number=0;
spacingRight: number=0;
spacingBottom: number=0;
spacingLeft: number=0;
horizontal: boolean=true;
background: string | null=null;
border: string | null=null;
wrap: boolean=false;
clipped: boolean=false;
overflow: string='visible';
labelPadding: number=0;
textDirection;
margin: mxRectangle | null=null;
unrotatedBoundingBox: mxRectangle | null=null;
flipH: boolean=false;
flipV: boolean=false;
/** /**
* Variable: baseSpacingTop * Variable: baseSpacingTop
* *
@ -78,15 +208,6 @@ class mxText extends mxShape {
*/ */
ignoreStringSize: boolean = false; ignoreStringSize: boolean = false;
/**
* Variable: textWidthPadding
*
* Specifies the padding to be added to the text width for the bounding box.
* This is needed to make sure no clipping is applied to borders. Default is 4
* for IE 8 standards mode and 3 for all others.
*/
textWidthPadding: number = 3;
/** /**
* Variable: lastValue * Variable: lastValue
* *
@ -101,124 +222,8 @@ class mxText extends mxShape {
*/ */
cacheEnabled: boolean = true; cacheEnabled: boolean = true;
/**
* Class: mxText
*
* Extends <mxShape> to implement a text shape. To change vertical text from
* bottom to top to top to bottom, the following code can be used:
*
* (code)
* verticalTextRotation = 90;
* (end)
*
* Constructor: mxText
*
* Constructs a new text shape.
*
* Parameters:
*
* value - String that represents the text to be displayed. This is stored in
* <value>.
* bounds - <mxRectangle> that defines the bounds. This is stored in
* <mxShape.bounds>.
* align - Specifies the horizontal alignment. Default is ''. This is stored in
* <align>.
* valign - Specifies the vertical alignment. Default is ''. This is stored in
* <valign>.
* color - String that specifies the text color. Default is 'black'. This is
* stored in <color>.
* family - String that specifies the font family. Default is
* <mxConstants.DEFAULT_FONTFAMILY>. This is stored in <family>.
* size - Integer that specifies the font size. Default is
* <mxConstants.DEFAULT_FONTSIZE>. This is stored in <size>.
* fontStyle - Specifies the font style. Default is 0. This is stored in
* <fontStyle>.
* spacing - Integer that specifies the global spacing. Default is 2. This is
* stored in <spacing>.
* spacingTop - Integer that specifies the top spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingTop>.
* spacingRight - Integer that specifies the right spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingRight>.
* spacingBottom - Integer that specifies the bottom spacing. Default is 0.The
* sum of the spacing and this is stored in <spacingBottom>.
* spacingLeft - Integer that specifies the left spacing. Default is 0. The
* sum of the spacing and this is stored in <spacingLeft>.
* horizontal - Boolean that specifies if the label is horizontal. Default is
* true. This is stored in <horizontal>.
* background - String that specifies the background color. Default is null.
* This is stored in <background>.
* border - String that specifies the label border color. Default is null.
* This is stored in <border>.
* wrap - Specifies if word-wrapping should be enabled. Default is false.
* This is stored in <wrap>.
* clipped - Specifies if the label should be clipped. Default is false.
* This is stored in <clipped>.
* overflow - Value of the overflow style. Default is 'visible'.
*/
constructor(
value,
bounds,
align,
valign,
color,
family,
size,
fontStyle,
spacing,
spacingTop,
spacingRight,
spacingBottom,
spacingLeft,
horizontal,
background,
border,
wrap,
clipped,
overflow,
labelPadding,
textDirection
) {
super();
this.value = value;
this.bounds = bounds;
this.color = color != null ? color : 'black';
this.align = align != null ? align : mxConstants.ALIGN_CENTER;
this.valign = valign != null ? valign : mxConstants.ALIGN_MIDDLE;
this.family = family != null ? family : mxConstants.DEFAULT_FONTFAMILY;
this.size = size != null ? size : mxConstants.DEFAULT_FONTSIZE;
this.fontStyle =
fontStyle != null ? fontStyle : mxConstants.DEFAULT_FONTSTYLE;
this.spacing = parseInt(spacing || 2);
this.spacingTop = this.spacing + parseInt(spacingTop || 0);
this.spacingRight = this.spacing + parseInt(spacingRight || 0);
this.spacingBottom = this.spacing + parseInt(spacingBottom || 0);
this.spacingLeft = this.spacing + parseInt(spacingLeft || 0);
this.horizontal = horizontal != null ? horizontal : true;
this.background = background;
this.border = border;
this.wrap = wrap != null ? wrap : false;
this.clipped = clipped != null ? clipped : false;
this.overflow = overflow != null ? overflow : 'visible';
this.labelPadding = labelPadding != null ? labelPadding : 0;
this.textDirection = textDirection;
this.rotation = 0;
this.updateMargin();
}
/**
* Function: isHtmlAllowed
*
* Returns true if HTML is allowed for this shape. This implementation returns
* true if the browser is not in IE8 standards mode.
*/
isHtmlAllowed(): boolean {
return document.documentMode !== 8 || mxClient.IS_EM;
}
/** /**
* Function: getSvgScreenOffset * Function: getSvgScreenOffset
*
* Disables offset in IE9 for crisper image output.
*/ */
getSvgScreenOffset(): number { getSvgScreenOffset(): number {
return 0; return 0;
@ -342,32 +347,17 @@ class mxText extends mxShape {
this.dialect === mxConstants.DIALECT_STRICTHTML) this.dialect === mxConstants.DIALECT_STRICTHTML)
) { ) {
if (this.node.nodeName === 'DIV') { if (this.node.nodeName === 'DIV') {
if (mxClient.IS_SVG) { this.redrawHtmlShape();
this.redrawHtmlShapeWithCss3();
} else {
this.updateSize(
this.node,
this.state == null || this.state.view.textDiv == null
);
this.updateHtmlTransform();
}
this.updateBoundingBox(); this.updateBoundingBox();
} else { } else {
const canvas = this.createCanvas(); const canvas = this.createCanvas();
if (canvas != null && canvas.updateText != null) { // Specifies if events should be handled
// Specifies if events should be handled canvas.pointerEvents = this.pointerEvents;
canvas.pointerEvents = this.pointerEvents;
this.paint(canvas, true); this.paint(canvas, true);
this.destroyCanvas(canvas); this.destroyCanvas(canvas);
this.updateBoundingBox(); this.updateBoundingBox();
} else {
// Fallback if canvas does not support updateText (VML)
super.redraw();
}
} }
} else { } else {
super.redraw(); super.redraw();
@ -406,7 +396,7 @@ class mxText extends mxShape {
delete this.background; delete this.background;
delete this.border; delete this.border;
this.textDirection = mxConstants.DEFAULT_TEXT_DIRECTION; this.textDirection = mxConstants.DEFAULT_TEXT_DIRECTION;
delete this.margin; this.margin = null;
} }
/** /**
@ -553,7 +543,6 @@ class mxText extends mxShape {
result = result.firstChild.firstChild.firstChild.firstChild.firstChild; result = result.firstChild.firstChild.firstChild.firstChild.firstChild;
} }
} }
return result; return result;
} }
@ -596,74 +585,39 @@ class mxText extends mxShape {
let ow = null; let ow = null;
let oh = null; let oh = null;
if (node.ownerSVGElement != null) { if (
if ( node.firstChild != null &&
node.firstChild != null && node.firstChild.firstChild != null &&
node.firstChild.firstChild != null && node.firstChild.firstChild.nodeName === 'foreignObject'
node.firstChild.firstChild.nodeName === 'foreignObject' ) {
) { // Uses second inner DIV for font metrics
// Uses second inner DIV for font metrics node = node.firstChild.firstChild.firstChild.firstChild;
node = node.firstChild.firstChild.firstChild.firstChild; oh = node.offsetHeight * this.scale;
oh = node.offsetHeight * this.scale;
if (this.overflow === 'width') { if (this.overflow === 'width') {
ow = this.boundingBox.width; ow = this.boundingBox.width;
} else {
ow = node.offsetWidth * this.scale;
}
} else { } else {
try { ow = node.offsetWidth * this.scale;
const b = node.getBBox();
// Workaround for bounding box of empty string
if (
typeof this.value === 'string' &&
mxUtils.trim(this.value) == 0
) {
this.boundingBox = null;
} else if (b.width === 0 && b.height === 0) {
this.boundingBox = null;
} else {
this.boundingBox = new mxRectangle(b.x, b.y, b.width, b.height);
}
return;
} catch (e) {
// Ignores NS_ERROR_FAILURE in FF if container display is none.
}
} }
} else { } else {
const td = this.state != null ? this.state.view.textDiv : null; try {
const b = node.getBBox();
// Use cached offset size
if (this.offsetWidth != null && this.offsetHeight != null) {
ow = this.offsetWidth * this.scale;
oh = this.offsetHeight * this.scale;
} else {
// Cannot get node size while container hidden so a
// shared temporary DIV is used for text measuring
if (td != null) {
this.updateFont(td);
this.updateSize(td, false);
this.updateInnerHtml(td);
node = td;
}
let sizeDiv = node;
// Workaround for bounding box of empty string
if ( if (
sizeDiv.firstChild != null && typeof this.value === 'string' &&
sizeDiv.firstChild.nodeName === 'DIV' mxUtils.trim(this.value) == 0
) { ) {
sizeDiv = sizeDiv.firstChild; this.boundingBox = null;
} else if (b.width === 0 && b.height === 0) {
this.boundingBox = null;
} else {
this.boundingBox = new mxRectangle(b.x, b.y, b.width, b.height);
} }
this.offsetWidth = sizeDiv.offsetWidth + this.textWidthPadding; return;
this.offsetHeight = sizeDiv.offsetHeight; } catch (e) {
// Ignores NS_ERROR_FAILURE in FF if container display is none.
ow = this.offsetWidth * this.scale;
oh = this.offsetHeight * this.scale;
} }
} }
@ -680,7 +634,7 @@ class mxText extends mxShape {
if (this.boundingBox != null) { if (this.boundingBox != null) {
if (rot !== 0) { if (rot !== 0) {
// Accounts for pre-rotated x and y // Accounts for pre-rotated x and y
const bbox = mxUtils.getBoundingBox( const bbox = <mxRectangle>mxUtils.getBoundingBox(
new mxRectangle( new mxRectangle(
this.margin.x * this.boundingBox.width, this.margin.x * this.boundingBox.width,
this.margin.y * this.boundingBox.height, this.margin.y * this.boundingBox.height,
@ -836,37 +790,6 @@ class mxText extends mxShape {
* Updates the HTML node(s) to reflect the latest bounds and scale. * Updates the HTML node(s) to reflect the latest bounds and scale.
*/ */
redrawHtmlShape() { redrawHtmlShape() {
if (mxClient.IS_SVG) {
this.redrawHtmlShapeWithCss3();
} else {
const { style } = this.node;
// Resets CSS styles
style.whiteSpace = 'normal';
style.overflow = '';
style.width = '';
style.height = '';
this.updateValue();
this.updateFont(this.node);
this.updateSize(
this.node,
this.state == null || this.state.view.textDiv == null
);
this.offsetWidth = null;
this.offsetHeight = null;
this.updateHtmlTransform();
}
}
/**
* Function: redrawHtmlShapeWithCss3
*
* Updates the HTML node(s) to reflect the latest bounds and scale.
*/
redrawHtmlShapeWithCss3() {
const w = Math.max(0, Math.round(this.bounds.width / this.scale)); const w = Math.max(0, Math.round(this.bounds.width / this.scale));
const h = Math.max(0, Math.round(this.bounds.height / this.scale)); const h = Math.max(0, Math.round(this.bounds.height / this.scale));
const flex = const flex =
@ -931,56 +854,6 @@ class mxText extends mxShape {
); );
} }
/**
* Function: updateHtmlTransform
*
* Returns the spacing as an <mxPoint>.
*/
updateHtmlTransform() {
const theta = this.getTextRotation();
const { style } = this.node;
const dx = this.margin.x;
const dy = this.margin.y;
if (theta !== 0) {
mxUtils.setPrefixedStyle(
style,
'transformOrigin',
`${-dx * 100}%` + ` ${-dy * 100}%`
);
mxUtils.setPrefixedStyle(
style,
'transform',
`translate(${dx * 100}%` +
`,${dy * 100}%) ` +
`scale(${this.scale}) rotate(${theta}deg)`
);
} else {
mxUtils.setPrefixedStyle(style, 'transformOrigin', '0% 0%');
mxUtils.setPrefixedStyle(
style,
'transform',
`scale(${this.scale}) ` + `translate(${dx * 100}%` + `,${dy * 100}%)`
);
}
style.left = `${Math.round(
this.bounds.x -
Math.ceil(
dx * (this.overflow !== 'fill' && this.overflow !== 'width' ? 3 : 1)
)
)}px`;
style.top = `${Math.round(
this.bounds.y - dy * (this.overflow !== 'fill' ? 3 : 1)
)}px`;
if (this.opacity < 100) {
style.opacity = this.opacity / 100;
} else {
style.opacity = '';
}
}
/** /**
* Function: updateInnerHtml * Function: updateInnerHtml
* *
@ -1006,159 +879,6 @@ class mxText extends mxShape {
} }
} }
/**
* Function: updateHtmlFilter
*
* Rotated text rendering quality is bad for IE9 quirks/IE8 standards
*/
updateHtmlFilter() {
const { style } = this.node;
const dx = this.margin.x;
let dy = this.margin.y;
const s = this.scale;
// Resets filter before getting offsetWidth
mxUtils.setOpacity(this.node, this.opacity);
// Adds 1 to match table height in 1.x
let ow = 0;
let oh = 0;
const td = this.state != null ? this.state.view.textDiv : null;
let sizeDiv = this.node;
// Fallback for hidden text rendering in IE quirks mode
if (td != null) {
td.style.overflow = '';
td.style.height = '';
td.style.width = '';
this.updateFont(td);
this.updateSize(td, false);
this.updateInnerHtml(td);
const w = Math.round(this.bounds.width / this.scale);
if (this.wrap && w > 0) {
td.style.whiteSpace = 'normal';
td.style.wordWrap = mxConstants.WORD_WRAP;
ow = w;
if (this.clipped) {
ow = Math.min(ow, this.bounds.width);
}
td.style.width = `${ow}px`;
} else {
td.style.whiteSpace = 'nowrap';
}
sizeDiv = td;
if (sizeDiv.firstChild != null && sizeDiv.firstChild.nodeName === 'DIV') {
sizeDiv = sizeDiv.firstChild;
if (this.wrap && td.style.wordWrap === 'break-word') {
sizeDiv.style.width = '100%';
}
}
// Required to update the height of the text box after wrapping width is known
if (!this.clipped && this.wrap && w > 0) {
ow = sizeDiv.offsetWidth + this.textWidthPadding;
td.style.width = `${ow}px`;
}
oh = sizeDiv.offsetHeight + 2;
} else if (
sizeDiv.firstChild != null &&
sizeDiv.firstChild.nodeName === 'DIV'
) {
sizeDiv = sizeDiv.firstChild;
oh = sizeDiv.offsetHeight;
}
ow = sizeDiv.offsetWidth + this.textWidthPadding;
if (this.clipped) {
oh = Math.min(oh, this.bounds.height);
}
let w = this.bounds.width / s;
let h = this.bounds.height / s;
// Handles special case for live preview with no wrapper DIV and no textDiv
if (this.overflow === 'fill') {
oh = h;
ow = w;
} else if (this.overflow === 'width') {
oh = sizeDiv.scrollHeight;
ow = w;
}
// Stores for later use
this.offsetWidth = ow;
this.offsetHeight = oh;
h = oh;
if (this.overflow !== 'fill' && this.overflow !== 'width') {
if (this.clipped) {
ow = Math.min(w, ow);
}
w = ow;
}
h *= s;
w *= s;
// Rotation case is handled via VML canvas
let rad = this.getTextRotation() * (Math.PI / 180);
// Precalculate cos and sin for the rotation
const real_cos = parseFloat(parseFloat(Math.cos(rad)).toFixed(8));
const real_sin = parseFloat(parseFloat(Math.sin(-rad)).toFixed(8));
rad %= 2 * Math.PI;
if (rad < 0) {
rad += 2 * Math.PI;
}
rad %= Math.PI;
if (rad > Math.PI / 2) {
rad = Math.PI - rad;
}
const cos = Math.cos(rad);
const sin = Math.sin(-rad);
const tx = w * -(dx + 0.5);
const ty = h * -(dy + 0.5);
const top_fix = (h - h * cos + w * sin) / 2 + real_sin * tx - real_cos * ty;
const left_fix =
(w - w * cos + h * sin) / 2 - real_cos * tx - real_sin * ty;
if (rad !== 0) {
const f = `progid:DXImageTransform.Microsoft.Matrix(M11=${real_cos}, M12=${real_sin}, M21=${-real_sin}, M22=${real_cos}, sizingMethod='auto expand')`;
if (style.filter != null && style.filter.length > 0) {
style.filter += ` ${f}`;
} else {
style.filter = f;
}
}
// Workaround for rendering offsets
dy = 0;
style.zoom = s;
style.left = `${Math.round(this.bounds.x + left_fix - w / 2)}px`;
style.top = `${Math.round(this.bounds.y + top_fix - h / 2 + dy)}px`;
}
/** /**
* Function: updateValue * Function: updateValue
* *

View File

@ -32,12 +32,10 @@ import mxSvgCanvas2D from "../../util/canvas/mxSvgCanvas2D";
* 1. This is stored in <strokewidth>. * 1. This is stored in <strokewidth>.
*/ */
class mxRectangleShape extends mxShape { class mxRectangleShape extends mxShape {
strokewidth: number;
constructor( constructor(
bounds: mxRectangle | null=null, bounds: mxRectangle | null=null,
fill: string = '#FFFFFF', fill: string | null = '#FFFFFF',
stroke: string = '#000000', stroke: string | null = '#000000',
strokewidth: number = 1 strokewidth: number = 1
) { ) {
super(); super();
@ -47,6 +45,9 @@ class mxRectangleShape extends mxShape {
this.strokewidth = strokewidth; this.strokewidth = strokewidth;
} }
// TODO: Document me!
strokewidth: number;
/** /**
* Function: isHtmlAllowed * Function: isHtmlAllowed
* *

View File

@ -15,31 +15,31 @@ import mxGraph from "../../view/graph/mxGraph";
class mxCellState extends mxRectangle { class mxCellState extends mxRectangle {
// TODO: Document me!! // TODO: Document me!!
cellBounds: mxRectangle | undefined; cellBounds: mxRectangle | null = null;
paintBounds: mxRectangle | undefined; paintBounds: mxRectangle | null = null;
boundingBox: mxRectangle | undefined; boundingBox: mxRectangle | null = null;
// Used by mxCellRenderer's createControl() // Used by mxCellRenderer's createControl()
control: mxShape | undefined; control: mxShape | null = null;
// Used by mxCellRenderer's createCellOverlays() // Used by mxCellRenderer's createCellOverlays()
overlays: any[] | null | undefined; overlays: any[] | null = null;
/** /**
* Variable: view * Variable: view
* *
* Reference to the enclosing <mxGraphView>. * Reference to the enclosing <mxGraphView>.
*/ */
view: mxGraphView | null = null; view: mxGraphView;
/** /**
* Variable: cell * Variable: cell
* *
* Reference to the <mxCell> that is represented by this state. * Reference to the <mxCell> that is represented by this state.
*/ */
cell: mxCell | null = null; cell: mxCell;
/** /**
* Variable: style * Variable: style
@ -47,7 +47,7 @@ class mxCellState extends mxRectangle {
* Contains an array of key, value pairs that represent the style of the * Contains an array of key, value pairs that represent the style of the
* cell. * cell.
*/ */
style: any | null = null; // TODO: Important - make the style type more strictly typed to allow for typescript checking of individual properties!!! style: any; // TODO: Important - make the style type more strictly typed to allow for typescript checking of individual properties!!!
/** /**
* Variable: invalidStyle * Variable: invalidStyle
@ -69,7 +69,7 @@ class mxCellState extends mxRectangle {
* <mxPoint> that holds the origin for all child cells. Default is a new * <mxPoint> that holds the origin for all child cells. Default is a new
* empty <mxPoint>. * empty <mxPoint>.
*/ */
origin: mxPoint | null = null; origin: mxPoint;
/** /**
* Variable: absolutePoints * Variable: absolutePoints
@ -86,7 +86,7 @@ class mxCellState extends mxRectangle {
* absolute coordinates of the label position. For vertices, this is the * absolute coordinates of the label position. For vertices, this is the
* offset of the label relative to the top, left corner of the vertex. * offset of the label relative to the top, left corner of the vertex.
*/ */
absoluteOffset: mxPoint | null = null; absoluteOffset: mxPoint;
/** /**
* Variable: visibleSourceState * Variable: visibleSourceState

View File

@ -488,7 +488,7 @@ class mxCell {
* *
* child - Child whose index should be returned. * child - Child whose index should be returned.
*/ */
getIndex(child: mxCell): number { getIndex(child: mxCell | null): number {
return mxUtils.indexOf(this.children, child); return mxUtils.indexOf(this.children, child);
} }

View File

@ -14,8 +14,141 @@ import mxGraph from '../graph/mxGraph';
import mxCell from './mxCell'; import mxCell from './mxCell';
import mxMouseEvent from '../../util/event/mxMouseEvent'; import mxMouseEvent from '../../util/event/mxMouseEvent';
import mxCellState from '../../util/datatypes/mxCellState'; import mxCellState from '../../util/datatypes/mxCellState';
import mxShape from "../../shape/mxShape";
import mxEventObject from "../../util/event/mxEventObject";
/**
* Class: mxCellEditor
*
* In-place editor for the graph. To control this editor, use
* <mxGraph.invokesStopCellEditing>, <mxGraph.enterStopsCellEditing> and
* <mxGraph.escapeEnabled>. If <mxGraph.enterStopsCellEditing> is true then
* ctrl-enter or shift-enter can be used to create a linefeed. The F2 and
* escape keys can always be used to stop editing.
*
* To customize the location of the textbox in the graph, override
* <getEditorBounds> as follows:
*
* (code)
* graph.cellEditor.getEditorBounds = (state)=>
* {
* let result = getEditorBounds.apply(this, arguments);
*
* if (this.graph.getModel().isEdge(state.cell))
* {
* result.x = state.getCenterX() - result.width / 2;
* result.y = state.getCenterY() - result.height / 2;
* }
*
* return result;
* };
* (end)
*
* Note that this hook is only called if <autoSize> is false. If <autoSize> is true,
* then <mxShape.getLabelBounds> is used to compute the current bounds of the textbox.
*
* The textarea uses the mxCellEditor CSS class. You can modify this class in
* your custom CSS. Note: You should modify the CSS after loading the client
* in the page.
*
* Example:
*
* To only allow numeric input in the in-place editor, use the following code.
*
* (code)
* let text = graph.cellEditor.textarea;
*
* mxEvent.addListener(text, 'keydown', function (evt)
* {
* if (!(evt.keyCode >= 48 && evt.keyCode <= 57) &&
* !(evt.keyCode >= 96 && evt.keyCode <= 105))
* {
* mxEvent.consume(evt);
* }
* });
* (end)
*
* Placeholder:
*
* To implement a placeholder for cells without a label, use the
* <emptyLabelText> variable.
*
* Resize in Chrome:
*
* Resize of the textarea is disabled by default. If you want to enable
* this feature extend <init> and set this.textarea.style.resize = ''.
*
* To start editing on a key press event, the container of the graph
* should have focus or a focusable parent should be used to add the
* key press handler as follows.
*
* (code)
* mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, (evt)=>
* {
* if (!graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 &&
* !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt))
* {
* graph.startEditing();
*
* if (mxClient.IS_FF)
* {
* graph.cellEditor.textarea.value = String.fromCharCode(evt.which);
* }
* }
* }));
* (end)
*
* To allow focus for a DIV, and hence to receive key press events, some browsers
* require it to have a valid tabindex attribute. In this case the following
* code may be used to keep the container focused.
*
* (code)
* let graphFireMouseEvent = graph.fireMouseEvent;
* graph.fireMouseEvent = (evtName, me, sender)=>
* {
* if (evtName == mxEvent.MOUSE_DOWN)
* {
* this.container.focus();
* }
*
* graphFireMouseEvent.apply(this, arguments);
* };
* (end)
*
* Constructor: mxCellEditor
*
* Constructs a new in-place editor for the specified graph.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
class mxCellEditor { class mxCellEditor {
constructor(graph: mxGraph) {
this.graph = graph;
// Stops editing after zoom changes
this.zoomHandler = () => {
if (this.graph.isEditing()) {
this.resize();
}
};
// Handling of deleted cells while editing
this.changeHandler = (sender: any) => {
if (
this.editingCell != null &&
this.graph.getView().getState(this.editingCell, false) == null
) {
this.stopEditing(true);
}
};
this.graph.view.addListener(mxEvent.SCALE, this.zoomHandler);
this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.zoomHandler);
this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
}
// TODO: Document me! // TODO: Document me!
changeHandler: Function | null; changeHandler: Function | null;
@ -25,16 +158,16 @@ class mxCellEditor {
bounds: mxRectangle | null = null; bounds: mxRectangle | null = null;
resizeThread: number | null; resizeThread: number | null = null;
textDirection: '' | 'auto' | 'ltr' | 'rtl' | null; textDirection: '' | 'auto' | 'ltr' | 'rtl' | null = null;
/** /**
* Variable: graph * Variable: graph
* *
* Reference to the enclosing <mxGraph>. * Reference to the enclosing <mxGraph>.
*/ */
graph: mxGraph = null; graph: mxGraph;
/** /**
* Variable: textarea * Variable: textarea
@ -42,21 +175,21 @@ class mxCellEditor {
* Holds the DIV that is used for text editing. Note that this may be null before the first * Holds the DIV that is used for text editing. Note that this may be null before the first
* edit. Instantiated in <init>. * edit. Instantiated in <init>.
*/ */
textarea: HTMLElement = null; textarea: HTMLElement | null = null;
/** /**
* Variable: editingCell * Variable: editingCell
* *
* Reference to the <mxCell> that is currently being edited. * Reference to the <mxCell> that is currently being edited.
*/ */
editingCell: mxCell = null; editingCell: mxCell | null = null;
/** /**
* Variable: trigger * Variable: trigger
* *
* Reference to the event that was used to start editing. * Reference to the event that was used to start editing.
*/ */
trigger: mxMouseEvent = null; trigger: mxMouseEvent | null = null;
/** /**
* Variable: modified * Variable: modified
@ -106,7 +239,7 @@ class mxCellEditor {
* *
* Reference to the label DOM node that has been hidden. * Reference to the label DOM node that has been hidden.
*/ */
textNode: HTMLElement | null = null; textNode: SVGGElement | null = null;
/** /**
* Variable: zIndex * Variable: zIndex
@ -153,137 +286,6 @@ class mxCellEditor {
*/ */
align: string | null = null; align: string | null = null;
/**
* Class: mxCellEditor
*
* In-place editor for the graph. To control this editor, use
* <mxGraph.invokesStopCellEditing>, <mxGraph.enterStopsCellEditing> and
* <mxGraph.escapeEnabled>. If <mxGraph.enterStopsCellEditing> is true then
* ctrl-enter or shift-enter can be used to create a linefeed. The F2 and
* escape keys can always be used to stop editing.
*
* To customize the location of the textbox in the graph, override
* <getEditorBounds> as follows:
*
* (code)
* graph.cellEditor.getEditorBounds = (state)=>
* {
* let result = getEditorBounds.apply(this, arguments);
*
* if (this.graph.getModel().isEdge(state.cell))
* {
* result.x = state.getCenterX() - result.width / 2;
* result.y = state.getCenterY() - result.height / 2;
* }
*
* return result;
* };
* (end)
*
* Note that this hook is only called if <autoSize> is false. If <autoSize> is true,
* then <mxShape.getLabelBounds> is used to compute the current bounds of the textbox.
*
* The textarea uses the mxCellEditor CSS class. You can modify this class in
* your custom CSS. Note: You should modify the CSS after loading the client
* in the page.
*
* Example:
*
* To only allow numeric input in the in-place editor, use the following code.
*
* (code)
* let text = graph.cellEditor.textarea;
*
* mxEvent.addListener(text, 'keydown', function (evt)
* {
* if (!(evt.keyCode >= 48 && evt.keyCode <= 57) &&
* !(evt.keyCode >= 96 && evt.keyCode <= 105))
* {
* mxEvent.consume(evt);
* }
* });
* (end)
*
* Placeholder:
*
* To implement a placeholder for cells without a label, use the
* <emptyLabelText> variable.
*
* Resize in Chrome:
*
* Resize of the textarea is disabled by default. If you want to enable
* this feature extend <init> and set this.textarea.style.resize = ''.
*
* To start editing on a key press event, the container of the graph
* should have focus or a focusable parent should be used to add the
* key press handler as follows.
*
* (code)
* mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, (evt)=>
* {
* if (!graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 &&
* !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt))
* {
* graph.startEditing();
*
* if (mxClient.IS_FF)
* {
* graph.cellEditor.textarea.value = String.fromCharCode(evt.which);
* }
* }
* }));
* (end)
*
* To allow focus for a DIV, and hence to receive key press events, some browsers
* require it to have a valid tabindex attribute. In this case the following
* code may be used to keep the container focused.
*
* (code)
* let graphFireMouseEvent = graph.fireMouseEvent;
* graph.fireMouseEvent = (evtName, me, sender)=>
* {
* if (evtName == mxEvent.MOUSE_DOWN)
* {
* this.container.focus();
* }
*
* graphFireMouseEvent.apply(this, arguments);
* };
* (end)
*
* Constructor: mxCellEditor
*
* Constructs a new in-place editor for the specified graph.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
*/
constructor(graph: mxGraph) {
this.graph = graph;
// Stops editing after zoom changes
this.zoomHandler = () => {
if (this.graph.isEditing()) {
this.resize();
}
};
// Handling of deleted cells while editing
this.changeHandler = sender => {
if (
this.editingCell != null &&
this.graph.getView().getState(this.editingCell, false) == null
) {
this.stopEditing(true);
}
};
this.graph.view.addListener(mxEvent.SCALE, this.zoomHandler);
this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.zoomHandler);
this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
}
/** /**
* Function: init * Function: init
* *
@ -309,8 +311,8 @@ class mxCellEditor {
* *
* Called in <stopEditing> if cancel is false to invoke <mxGraph.labelChanged>. * Called in <stopEditing> if cancel is false to invoke <mxGraph.labelChanged>.
*/ */
applyValue(state: mxCellState, value): void { applyValue(state: mxCellState, value: any): void {
this.graph.labelChanged(state.cell, value, this.trigger); this.graph.labelChanged(state.cell, value, <mxMouseEvent>this.trigger);
} }
/** /**
@ -318,7 +320,7 @@ class mxCellEditor {
* *
* Sets the temporary horizontal alignment for the current editing session. * Sets the temporary horizontal alignment for the current editing session.
*/ */
setAlign(align) { setAlign(align: string): void {
if (this.textarea != null) { if (this.textarea != null) {
this.textarea.style.textAlign = align; this.textarea.style.textAlign = align;
} }
@ -332,7 +334,7 @@ class mxCellEditor {
* *
* Gets the initial editing value for the given cell. * Gets the initial editing value for the given cell.
*/ */
getInitialValue(state, trigger) { getInitialValue(state: mxCellState, trigger: mxEventObject | mxMouseEvent) {
let result = mxUtils.htmlEntities( let result = mxUtils.htmlEntities(
this.graph.getEditingValue(state.cell, trigger), this.graph.getEditingValue(state.cell, trigger),
false false
@ -347,6 +349,7 @@ class mxCellEditor {
* Returns the current editing value. * Returns the current editing value.
*/ */
getCurrentValue(state: mxCellState) { getCurrentValue(state: mxCellState) {
// @ts-ignore
return mxUtils.extractTextWithWhitespace(this.textarea.childNodes); return mxUtils.extractTextWithWhitespace(this.textarea.childNodes);
} }
@ -356,7 +359,7 @@ class mxCellEditor {
* Returns true if <escapeCancelsEditing> is true and shift, control and meta * Returns true if <escapeCancelsEditing> is true and shift, control and meta
* are not pressed. * are not pressed.
*/ */
isCancelEditingKeyEvent(evt) { isCancelEditingKeyEvent(evt: KeyboardEvent) {
return ( return (
this.escapeCancelsEditing || this.escapeCancelsEditing ||
mxEvent.isShiftDown(evt) || mxEvent.isShiftDown(evt) ||
@ -370,31 +373,31 @@ class mxCellEditor {
* *
* Installs listeners for focus, change and standard key event handling. * Installs listeners for focus, change and standard key event handling.
*/ */
installListeners(elt) { installListeners(elt: HTMLElement) {
// Applies value if text is dragged // Applies value if text is dragged
// LATER: Gesture mouse events ignored for starting move // LATER: Gesture mouse events ignored for starting move
mxEvent.addListener( mxEvent.addListener(
elt, elt,
'dragstart', 'dragstart',
mxUtils.bind(this, evt => { (evt: Event) => {
this.graph.stopEditing(false); this.graph.stopEditing(false);
mxEvent.consume(evt); mxEvent.consume(evt);
}) }
); );
// Applies value if focus is lost // Applies value if focus is lost
mxEvent.addListener( mxEvent.addListener(
elt, elt,
'blur', 'blur',
mxUtils.bind(this, evt => { (evt: Event) => {
if (this.blurEnabled) { if (this.blurEnabled) {
this.focusLost(); this.focusLost();
} }
}) }
); );
// Updates modified state and handles placeholder text // Updates modified state and handles placeholder text
mxEvent.addListener(elt, 'keydown', evt => { mxEvent.addListener(elt, 'keydown', (evt: KeyboardEvent) => {
if (!mxEvent.isConsumed(evt)) { if (!mxEvent.isConsumed(evt)) {
if (this.isStopEditingEvent(evt)) { if (this.isStopEditingEvent(evt)) {
this.graph.stopEditing(false); this.graph.stopEditing(false);
@ -407,7 +410,7 @@ class mxCellEditor {
}); });
// Keypress only fires if printable key was pressed and handles removing the empty placeholder // Keypress only fires if printable key was pressed and handles removing the empty placeholder
const keypressHandler = evt => { const keypressHandler = (evt: KeyboardEvent) => {
if (this.editingCell != null) { if (this.editingCell != null) {
// Clears the initial empty label on the first keystroke // Clears the initial empty label on the first keystroke
// and workaround for FF which fires keypress for delete and backspace // and workaround for FF which fires keypress for delete and backspace
@ -428,18 +431,20 @@ class mxCellEditor {
mxEvent.addListener(elt, 'paste', keypressHandler); mxEvent.addListener(elt, 'paste', keypressHandler);
// Handler for updating the empty label text value after a change // Handler for updating the empty label text value after a change
const keyupHandler = evt => { const keyupHandler = (evt: KeyboardEvent) => {
if (this.editingCell != null) { if (this.editingCell != null) {
// Uses an optional text value for sempty labels which is cleared // Uses an optional text value for sempty labels which is cleared
// when the first keystroke appears. This makes it easier to see // when the first keystroke appears. This makes it easier to see
// that a label is being edited even if the label is empty. // that a label is being edited even if the label is empty.
// In Safari and FF, an empty text is represented by <BR> which isn't enough to force a valid size // In Safari and FF, an empty text is represented by <BR> which isn't enough to force a valid size
const textarea = <HTMLElement>this.textarea;
if ( if (
this.textarea.innerHTML.length === 0 || textarea.innerHTML.length === 0 ||
this.textarea.innerHTML === '<br>' textarea.innerHTML === '<br>'
) { ) {
this.textarea.innerHTML = this.getEmptyLabelText(); textarea.innerHTML = this.getEmptyLabelText();
this.clearOnChange = this.textarea.innerHTML.length > 0; this.clearOnChange = textarea.innerHTML.length > 0;
} else { } else {
this.clearOnChange = false; this.clearOnChange = false;
} }
@ -453,7 +458,7 @@ class mxCellEditor {
// Adds automatic resizing of the textbox while typing using input, keyup and/or DOM change events // Adds automatic resizing of the textbox while typing using input, keyup and/or DOM change events
const evtName = 'input'; const evtName = 'input';
const resizeHandler = mxUtils.bind(this, evt => { const resizeHandler = (evt: Event) => {
if ( if (
this.editingCell != null && this.editingCell != null &&
this.autoSize && this.autoSize &&
@ -470,7 +475,7 @@ class mxCellEditor {
this.resize(); this.resize();
}, 0); }, 0);
} }
}); };
mxEvent.addListener(elt, evtName, resizeHandler); mxEvent.addListener(elt, evtName, resizeHandler);
mxEvent.addListener(window, 'resize', resizeHandler); mxEvent.addListener(window, 'resize', resizeHandler);
@ -485,7 +490,7 @@ class mxCellEditor {
* returns true if F2 is pressed of if <mxGraph.enterStopsCellEditing> is true * returns true if F2 is pressed of if <mxGraph.enterStopsCellEditing> is true
* and enter is pressed without control or shift. * and enter is pressed without control or shift.
*/ */
isStopEditingEvent(evt) { isStopEditingEvent(evt: KeyboardEvent) {
return ( return (
evt.keyCode === 113 /* F2 */ || evt.keyCode === 113 /* F2 */ ||
(this.graph.isEnterStopsCellEditing() && (this.graph.isEnterStopsCellEditing() &&
@ -500,7 +505,7 @@ class mxCellEditor {
* *
* Returns true if this editor is the source for the given native event. * Returns true if this editor is the source for the given native event.
*/ */
isEventSource(evt): boolean { isEventSource(evt: Event): boolean {
return mxEvent.getSource(evt) === this.textarea; return mxEvent.getSource(evt) === this.textarea;
} }
@ -519,7 +524,7 @@ class mxCellEditor {
if (!this.autoSize || state.style.overflow === 'fill') { if (!this.autoSize || state.style.overflow === 'fill') {
// Specifies the bounds of the editor box // Specifies the bounds of the editor box
this.bounds = this.getEditorBounds(state); this.bounds = <mxRectangle>this.getEditorBounds(state);
this.textarea.style.width = `${Math.round( this.textarea.style.width = `${Math.round(
this.bounds.width / scale this.bounds.width / scale
)}px`; )}px`;
@ -621,6 +626,7 @@ class mxCellEditor {
!state.view.graph.cellRenderer.legacySpacing || !state.view.graph.cellRenderer.legacySpacing ||
state.style[mxConstants.STYLE_OVERFLOW] !== 'width' state.style[mxConstants.STYLE_OVERFLOW] !== 'width'
) { ) {
// @ts-ignore
const dummy = new mxText(); // FIXME!!!! =================================================================================================== const dummy = new mxText(); // FIXME!!!! ===================================================================================================
const spacing = parseInt(state.style.spacing || 2) * scale; const spacing = parseInt(state.style.spacing || 2) * scale;
const spacingTop = const spacingTop =
@ -758,7 +764,7 @@ class mxCellEditor {
* Returns the background color for the in-place editor. This implementation * Returns the background color for the in-place editor. This implementation
* always returns null. * always returns null.
*/ */
getBackgroundColor(state): string | null { getBackgroundColor(state: mxCellState): string | null {
return null; return null;
} }
@ -772,7 +778,7 @@ class mxCellEditor {
* cell - <mxCell> to start editing. * cell - <mxCell> to start editing.
* trigger - Optional mouse event that triggered the editor. * trigger - Optional mouse event that triggered the editor.
*/ */
startEditing(cell: mxCell, trigger: MouseEvent | null = null): void { startEditing(cell: mxCell, trigger: mxMouseEvent | null = null): void {
this.stopEditing(true); this.stopEditing(true);
this.align = null; this.align = null;
@ -819,19 +825,20 @@ class mxCellEditor {
txtDecor.push('line-through'); txtDecor.push('line-through');
} }
this.textarea.style.lineHeight = mxConstants.ABSOLUTE_LINE_HEIGHT const textarea = <HTMLElement>this.textarea;
textarea.style.lineHeight = mxConstants.ABSOLUTE_LINE_HEIGHT
? `${Math.round(size * mxConstants.LINE_HEIGHT)}px` ? `${Math.round(size * mxConstants.LINE_HEIGHT)}px`
: String(mxConstants.LINE_HEIGHT); : String(mxConstants.LINE_HEIGHT);
this.textarea.style.backgroundColor = this.getBackgroundColor(state); textarea.style.backgroundColor = this.getBackgroundColor(state) || 'transparent';
this.textarea.style.textDecoration = txtDecor.join(' '); textarea.style.textDecoration = txtDecor.join(' ');
this.textarea.style.fontWeight = bold ? 'bold' : 'normal'; textarea.style.fontWeight = bold ? 'bold' : 'normal';
this.textarea.style.fontStyle = italic ? 'italic' : ''; textarea.style.fontStyle = italic ? 'italic' : '';
this.textarea.style.fontSize = `${Math.round(size)}px`; textarea.style.fontSize = `${Math.round(size)}px`;
this.textarea.style.zIndex = String(this.zIndex); textarea.style.zIndex = String(this.zIndex);
this.textarea.style.fontFamily = family; textarea.style.fontFamily = family;
this.textarea.style.textAlign = align; textarea.style.textAlign = align;
this.textarea.style.outline = 'none'; textarea.style.outline = 'none';
this.textarea.style.color = color; textarea.style.color = color;
let dir = (this.textDirection = let dir = (this.textDirection =
state.style.textDirection != null state.style.textDirection != null
@ -849,30 +856,31 @@ class mxCellEditor {
} }
if (dir === 'ltr' || dir === 'rtl') { if (dir === 'ltr' || dir === 'rtl') {
this.textarea.setAttribute('dir', dir); textarea.setAttribute('dir', dir);
} else { } else {
this.textarea.removeAttribute('dir'); textarea.removeAttribute('dir');
} }
// Sets the initial editing value // Sets the initial editing value
this.textarea.innerHTML = this.getInitialValue(state, trigger) || ''; textarea.innerHTML = this.getInitialValue(state, <mxMouseEvent>trigger) || '';
this.initialValue = this.textarea.innerHTML; this.initialValue = textarea.innerHTML;
// Uses an optional text value for empty labels which is cleared // Uses an optional text value for empty labels which is cleared
// when the first keystroke appears. This makes it easier to see // when the first keystroke appears. This makes it easier to see
// that a label is being edited even if the label is empty. // that a label is being edited even if the label is empty.
if ( if (
this.textarea.innerHTML.length === 0 || textarea.innerHTML.length === 0 ||
this.textarea.innerHTML === '<br>' textarea.innerHTML === '<br>'
) { ) {
this.textarea.innerHTML = this.getEmptyLabelText(); textarea.innerHTML = <string>this.getEmptyLabelText();
this.clearOnChange = true; this.clearOnChange = true;
} else { } else {
this.clearOnChange = this.clearOnChange =
this.textarea.innerHTML === this.getEmptyLabelText(); textarea.innerHTML === this.getEmptyLabelText();
} }
this.graph.container.appendChild(this.textarea); // @ts-ignore
this.graph.container.appendChild(textarea);
// Update this after firing all potential events that could update the cleanOnChange flag // Update this after firing all potential events that could update the cleanOnChange flag
this.editingCell = cell; this.editingCell = cell;
@ -880,13 +888,14 @@ class mxCellEditor {
this.textNode = null; this.textNode = null;
if (state.text != null && this.isHideLabel(state)) { if (state.text != null && this.isHideLabel(state)) {
this.textNode = state.text.node; this.textNode = <SVGGElement>state.text.node;
this.textNode.style.visibility = 'hidden'; this.textNode.style.visibility = 'hidden';
} }
// Workaround for initial offsetHeight not ready for heading in markup // Workaround for initial offsetHeight not ready for heading in markup
if ( if (
this.autoSize && this.autoSize &&
// @ts-ignore
(this.graph.model.isEdge(state.cell) || (this.graph.model.isEdge(state.cell) ||
state.style[mxConstants.STYLE_OVERFLOW] !== 'fill') state.style[mxConstants.STYLE_OVERFLOW] !== 'fill')
) { ) {
@ -903,15 +912,15 @@ class mxCellEditor {
// Workaround for NS_ERROR_FAILURE in FF // Workaround for NS_ERROR_FAILURE in FF
try { try {
// Prefers blinking cursor over no selected text if empty // Prefers blinking cursor over no selected text if empty
this.textarea.focus(); textarea.focus();
if ( if (
this.isSelectText() && this.isSelectText() &&
this.textarea.innerHTML.length > 0 && textarea.innerHTML.length > 0 &&
(this.textarea.innerHTML !== this.getEmptyLabelText() || (textarea.innerHTML !== this.getEmptyLabelText() ||
!this.clearOnChange) !this.clearOnChange)
) { ) {
document.execCommand('selectAll', false, null); document.execCommand('selectAll', false);
} }
} catch (e) { } catch (e) {
// ignore // ignore
@ -958,30 +967,31 @@ class mxCellEditor {
} }
const state = !cancel ? this.graph.view.getState(this.editingCell) : null; const state = !cancel ? this.graph.view.getState(this.editingCell) : null;
const textarea = <HTMLElement>this.textarea;
const initial = this.initialValue; const initial = this.initialValue;
this.initialValue = null; this.initialValue = null;
this.editingCell = null; this.editingCell = null;
this.trigger = null; this.trigger = null;
this.bounds = null; this.bounds = null;
this.textarea.blur(); textarea.blur();
this.clearSelection(); this.clearSelection();
if (this.textarea.parentNode != null) { if (textarea.parentNode != null) {
this.textarea.parentNode.removeChild(this.textarea); textarea.parentNode.removeChild(textarea);
} }
if ( if (
this.clearOnChange && this.clearOnChange &&
this.textarea.innerHTML === this.getEmptyLabelText() textarea.innerHTML === this.getEmptyLabelText()
) { ) {
this.textarea.innerHTML = ''; textarea.innerHTML = '';
this.clearOnChange = false; this.clearOnChange = false;
} }
if ( if (
state != null && state != null &&
(this.textarea.innerHTML !== initial || this.align != null) (textarea.innerHTML !== initial || this.align != null)
) { ) {
this.prepareTextarea(); this.prepareTextarea();
const value = this.getCurrentValue(state); const value = this.getCurrentValue(state);
@ -1016,11 +1026,12 @@ class mxCellEditor {
* This implementation removes the extra trailing linefeed in Firefox. * This implementation removes the extra trailing linefeed in Firefox.
*/ */
prepareTextarea(): void { prepareTextarea(): void {
const textarea = <HTMLElement>this.textarea;
if ( if (
this.textarea.lastChild != null && textarea.lastChild != null &&
this.textarea.lastChild.nodeName === 'BR' textarea.lastChild.nodeName === 'BR'
) { ) {
this.textarea.removeChild(this.textarea.lastChild); textarea.removeChild(textarea.lastChild);
} }
} }
@ -1041,12 +1052,13 @@ class mxCellEditor {
*/ */
getMinimumSize(state: mxCellState): mxRectangle { getMinimumSize(state: mxCellState): mxRectangle {
const { scale } = this.graph.getView(); const { scale } = this.graph.getView();
const textarea = <HTMLElement>this.textarea;
return new mxRectangle( return new mxRectangle(
0, 0,
0, 0,
state.text == null ? 30 : state.text.size * scale + 20, state.text == null ? 30 : state.text.size * scale + 20,
this.textarea.style.textAlign === 'left' ? 120 : 40 textarea.style.textAlign === 'left' ? 120 : 40
); );
} }
@ -1068,8 +1080,9 @@ class mxCellEditor {
state.view.graph.cellRenderer.legacySpacing && state.view.graph.cellRenderer.legacySpacing &&
state.style[mxConstants.STYLE_OVERFLOW] === 'fill' state.style[mxConstants.STYLE_OVERFLOW] === 'fill'
) { ) {
result = state.shape.getLabelBounds(mxRectangle.fromRectangle(state)); result = (<mxShape>state.shape).getLabelBounds(mxRectangle.fromRectangle(state));
} else { } else {
// @ts-ignore
const dummy = new mxText(); // FIXME!!!! =================================================================================================== const dummy = new mxText(); // FIXME!!!! ===================================================================================================
const spacing: number = parseInt(state.style.spacing || 0) * scale; const spacing: number = parseInt(state.style.spacing || 0) * scale;
const spacingTop: number = const spacingTop: number =
@ -1146,7 +1159,7 @@ class mxCellEditor {
// Applies the horizontal and vertical label positions // Applies the horizontal and vertical label positions
if (this.graph.getModel().isVertex(state.cell)) { if (this.graph.getModel().isVertex(state.cell)) {
const horizontal: string = mxUtils.getStringValue( const horizontal: string = <string>mxUtils.getStringValue(
state.style, state.style,
mxConstants.STYLE_LABEL_POSITION, mxConstants.STYLE_LABEL_POSITION,
mxConstants.ALIGN_CENTER mxConstants.ALIGN_CENTER
@ -1191,8 +1204,8 @@ class mxCellEditor {
* cell - <mxCell> for which a text for an empty editing box should be * cell - <mxCell> for which a text for an empty editing box should be
* returned. * returned.
*/ */
getEmptyLabelText(cell: mxCell | null = null): string | null { getEmptyLabelText(cell: mxCell | null = null): string {
return this.emptyLabelText; return this.emptyLabelText || '';
} }
/** /**

View File

@ -49,7 +49,7 @@ class mxCellOverlay extends mxEventSource {
* Holds the offset as an <mxPoint>. The offset will be scaled according to the * Holds the offset as an <mxPoint>. The offset will be scaled according to the
* current scale. * current scale.
*/ */
offset: mxPoint | null = null; offset: mxPoint = new mxPoint();
/** /**
* Variable: cursor * Variable: cursor
@ -172,11 +172,12 @@ class mxCellOverlay extends mxEventSource {
const s = state.view.scale; const s = state.view.scale;
let pt = null; let pt = null;
const w = this.image.width; const image = <mxImage>this.image;
const h = this.image.height; const w = image.width;
const h = image.height;
if (isEdge) { if (isEdge) {
const pts = state.absolutePoints; const pts = <mxPoint[]>state.absolutePoints;
if (pts.length % 2 === 1) { if (pts.length % 2 === 1) {
pt = pts[Math.floor(pts.length / 2)]; pt = pts[Math.floor(pts.length / 2)];

View File

@ -33,6 +33,7 @@ import mxPoint from '../../util/datatypes/mxPoint';
import mxShape from '../../shape/mxShape'; import mxShape from '../../shape/mxShape';
import mxCellState from '../../util/datatypes/mxCellState'; import mxCellState from '../../util/datatypes/mxCellState';
import mxCell from './mxCell'; import mxCell from './mxCell';
import mxGraphModel from "../graph/mxGraphModel";
class mxCellRenderer { class mxCellRenderer {
/** /**
@ -56,7 +57,7 @@ class mxCellRenderer {
* *
* Defines the default shape for vertices. Default is <mxRectangleShape>. * Defines the default shape for vertices. Default is <mxRectangleShape>.
*/ */
defaultVertexShape: typeof mxShape = mxRectangleShape; defaultVertexShape: typeof mxRectangleShape = mxRectangleShape;
/** /**
* Variable: defaultTextShape * Variable: defaultTextShape
@ -1532,7 +1533,8 @@ class mxCellRenderer {
force: boolean = false, force: boolean = false,
rendering: boolean = true rendering: boolean = true
): boolean { ): boolean {
const { model } = state.view.graph; const model = <mxGraphModel>state.view.graph.model;
let shapeChanged = false; let shapeChanged = false;
// Forces creation of new shape if shape style has changed // Forces creation of new shape if shape style has changed

View File

@ -10,6 +10,7 @@ import mxDictionary from '../../util/datatypes/mxDictionary';
import mxCellState from '../../util/datatypes/mxCellState'; import mxCellState from '../../util/datatypes/mxCellState';
import mxCell from './mxCell'; import mxCell from './mxCell';
import mxGraph from '../graph/mxGraph'; import mxGraph from '../graph/mxGraph';
import mxGraphView from "../graph/mxGraphView";
class mxCellStatePreview { class mxCellStatePreview {
/** /**
@ -17,14 +18,14 @@ class mxCellStatePreview {
* *
* Reference to the enclosing <mxGraph>. * Reference to the enclosing <mxGraph>.
*/ */
graph: mxGraph | null = null; graph: mxGraph;
/** /**
* Variable: deltas * Variable: deltas
* *
* Reference to the enclosing <mxGraph>. * Reference to the enclosing <mxGraph>.
*/ */
deltas: mxDictionary | null = null; deltas: mxDictionary;
/** /**
* Variable: count * Variable: count
@ -96,22 +97,18 @@ class mxCellStatePreview {
* Function: show * Function: show
*/ */
show(visitor: Function | null = null) { show(visitor: Function | null = null) {
this.deltas.visit( this.deltas.visit((key: string, delta: any) => {
mxUtils.bind(this, (key, delta) => { this.translateState(delta.state, delta.point.x, delta.point.y);
this.translateState(delta.state, delta.point.x, delta.point.y); });
})
);
this.deltas.visit( this.deltas.visit((key: string, delta: any) => {
mxUtils.bind(this, (key, delta) => { this.revalidateState(
this.revalidateState( delta.state,
delta.state, delta.point.x,
delta.point.x, delta.point.y,
delta.point.y, visitor
visitor );
); });
})
);
} }
/** /**
@ -122,7 +119,7 @@ class mxCellStatePreview {
const model = this.graph.getModel(); const model = this.graph.getModel();
if (model.isVertex(state.cell)) { if (model.isVertex(state.cell)) {
state.view.updateCellState(state); (<mxGraphView>state.view).updateCellState(state);
const geo = model.getGeometry(state.cell); const geo = model.getGeometry(state.cell);
// Moves selection cells and non-relative vertices in // Moves selection cells and non-relative vertices in
@ -142,7 +139,7 @@ class mxCellStatePreview {
for (let i = 0; i < childCount; i += 1) { for (let i = 0; i < childCount; i += 1) {
this.translateState( this.translateState(
state.view.getState(model.getChildAt(state.cell, i)), <mxCellState>(state.view).getState(model.getChildAt(state.cell, i)),
dx, dx,
dy dy
); );
@ -168,8 +165,8 @@ class mxCellStatePreview {
state.view.updateCellState(state); state.view.updateCellState(state);
} }
const geo = this.graph.getCellGeometry(state.cell); const geo = this.graph.getCellGeometry(<mxCell>state.cell);
const pState = state.view.getState(model.getParent(state.cell)); const pState = state.view.getState(model.getParent(<mxCell>state.cell));
// Moves selection vertices which are relative // Moves selection vertices which are relative
if ( if (
@ -210,10 +207,10 @@ class mxCellStatePreview {
*/ */
addEdges(state: mxCellState): void { addEdges(state: mxCellState): void {
const model = this.graph.getModel(); const model = this.graph.getModel();
const edgeCount = model.getEdgeCount(state.cell); const edgeCount = model.getEdgeCount(<mxCell>state.cell);
for (let i = 0; i < edgeCount; i += 1) { for (let i = 0; i < edgeCount; i += 1) {
const s = state.view.getState(model.getEdgeAt(state.cell, i)); const s = state.view.getState(model.getEdgeAt(<mxCell>state.cell, i));
if (s != null) { if (s != null) {
this.moveState(s, 0, 0); this.moveState(s, 0, 0);

View File

@ -63,7 +63,7 @@ class mxConnectionConstraint {
*/ */
constructor(point: mxPoint | null=null, constructor(point: mxPoint | null=null,
perimeter: boolean=true, perimeter: boolean=true,
name: string='', name: string | null=null,
dx: number | null=null, dx: number | null=null,
dy: number | null=null) { dy: number | null=null) {
this.point = point; this.point = point;

File diff suppressed because it is too large Load Diff

View File

@ -1094,7 +1094,7 @@ class mxGraphModel extends mxEventSource {
* isSource - Boolean indicating which end of the edge should be returned. * isSource - Boolean indicating which end of the edge should be returned.
*/ */
getTerminal(edge: mxCell | null, getTerminal(edge: mxCell | null,
isSource: boolean=false) { isSource: boolean=false): mxCell | null {
return edge != null ? edge.getTerminal(isSource) : null; return edge != null ? edge.getTerminal(isSource) : null;
} }
@ -1623,7 +1623,7 @@ class mxGraphModel extends mxEventSource {
* *
* cell - <mxCell> whose style should be returned. * cell - <mxCell> whose style should be returned.
*/ */
getStyle(cell: mxCell): any { getStyle(cell: mxCell | null): any {
return cell != null ? cell.getStyle() : null; return cell != null ? cell.getStyle() : null;
} }
@ -1730,7 +1730,7 @@ class mxGraphModel extends mxEventSource {
* *
* cell - <mxCell> whose visible state should be returned. * cell - <mxCell> whose visible state should be returned.
*/ */
isVisible(cell: mxCell): boolean { isVisible(cell: mxCell | null): boolean {
return cell != null ? cell.isVisible() : false; return cell != null ? cell.isVisible() : false;
} }

View File

@ -161,7 +161,7 @@ class mxGraphSelectionModel extends mxEventSource {
* *
* cell - <mxCell> to be selected. * cell - <mxCell> to be selected.
*/ */
setCell(cell: mxCell): void { setCell(cell: mxCell | null): void {
if (cell != null) { if (cell != null) {
this.setCells([cell]); this.setCells([cell]);
} }

View File

@ -101,7 +101,7 @@ class mxGraphView extends mxEventSource {
* *
* Returns reference to the enclosing <mxGraph>. * Returns reference to the enclosing <mxGraph>.
*/ */
graph: mxGraph | null = null; graph: mxGraph;
/** /**
* Variable: currentRoot * Variable: currentRoot
@ -1548,9 +1548,9 @@ class mxGraphView extends mxEventSource {
* associated. * associated.
*/ */
isLoopStyleEnabled(edge: mxCellState, isLoopStyleEnabled(edge: mxCellState,
points: mxPoint[], points: mxPoint[]=[],
source: mxCellState, source: mxCellState | null=null,
target: mxCellState): boolean { target: mxCellState | null=null): boolean {
const sc = (<mxGraph>this.graph).getConnectionConstraint(edge, source, true); const sc = (<mxGraph>this.graph).getConnectionConstraint(edge, source, true);
const tc = (<mxGraph>this.graph).getConnectionConstraint(edge, target, false); const tc = (<mxGraph>this.graph).getConnectionConstraint(edge, target, false);
@ -1575,9 +1575,9 @@ class mxGraphView extends mxEventSource {
* Returns the edge style function to be used to render the given edge state. * Returns the edge style function to be used to render the given edge state.
*/ */
getEdgeStyle(edge: mxCellState, getEdgeStyle(edge: mxCellState,
points: mxPoint[], points: mxPoint[]=[],
source: mxCellState, source: mxCellState | null=null,
target: mxCellState): any { target: mxCellState | null=null): any {
let edgeStyle: any = this.isLoopStyleEnabled(edge, points, source, target) let edgeStyle: any = this.isLoopStyleEnabled(edge, points, source, target)
? mxUtils.getValue( ? mxUtils.getValue(
@ -1970,7 +1970,7 @@ class mxGraphView extends mxEventSource {
* source - Boolean that specifies if the source or target terminal * source - Boolean that specifies if the source or target terminal
* should be returned. * should be returned.
*/ */
getVisibleTerminal(edge: mxCell, getVisibleTerminal(edge: mxCell | null,
source: boolean) { source: boolean) {
const model = (<mxGraph>this.graph).getModel(); const model = (<mxGraph>this.graph).getModel();

View File

@ -13,10 +13,175 @@ import mxGraph from './mxGraph';
import mxImageShape from '../../shape/node/mxImageShape'; import mxImageShape from '../../shape/node/mxImageShape';
import mxEvent from '../../util/event/mxEvent'; import mxEvent from '../../util/event/mxEvent';
import mxUtils from '../../util/mxUtils'; import mxUtils from '../../util/mxUtils';
import mxClient from '../../mxClient';
import mxImage from '../../util/image/mxImage'; import mxImage from '../../util/image/mxImage';
import mxEventObject from "../../util/event/mxEventObject";
/**
* Class: mxOutline
*
* Implements an outline (aka overview) for a graph. Set <updateOnPan> to true
* to enable updates while the source graph is panning.
*
* Example:
*
* (code)
* let outline = new mxOutline(graph, div);
* (end)
*
* To move the graph to the top, left corner the following code can be used.
*
* (code)
* let scale = graph.view.scale;
* let bounds = graph.getGraphBounds();
* graph.view.setTranslate(-bounds.x / scale, -bounds.y / scale);
* (end)
*
* To toggle the suspended mode, the following can be used.
*
* (code)
* outline.suspended = !outln.suspended;
* if (!outline.suspended)
* {
* outline.update(true);
* }
* (end)
*
* Constructor: mxOutline
*
* Constructs a new outline for the specified graph inside the given
* container.
*
* Parameters:
*
* source - <mxGraph> to create the outline for.
* container - DOM node that will contain the outline.
*/
class mxOutline { class mxOutline {
constructor(source: mxGraph, container: HTMLElement | null = null) {
this.source = source;
if (container != null) {
this.init(container);
}
}
/**
* Function: init
*
* Initializes the outline inside the given container.
*/
init(container: HTMLElement) {
this.outline = this.createGraph(container);
// Do not repaint when suspended
const outlineGraphModelChanged = this.outline.graphModelChanged;
this.outline.graphModelChanged = mxUtils.bind(this, (changes: any) => {
if (!this.suspended && this.outline != null) {
outlineGraphModelChanged.apply(this.outline, [changes]);
}
});
// Enable faster painting in SVG
//const node = <SVGElement>this.outline.getView().getCanvas().parentNode;
//node.setAttribute('shape-rendering', 'optimizeSpeed');
//node.setAttribute('image-rendering', 'optimizeSpeed');
// Hides cursors and labels
this.outline.labelsVisible = this.labelsVisible;
this.outline.setEnabled(false);
this.updateHandler = (sender: any, evt: mxEventObject) => {
if (!this.suspended && !this.active) {
this.update();
}
};
// Updates the scale of the outline after a change of the main graph
this.source.getModel().addListener(mxEvent.CHANGE, this.updateHandler);
this.outline.addMouseListener(this);
// Adds listeners to keep the outline in sync with the source graph
const view = this.source.getView();
view.addListener(mxEvent.SCALE, this.updateHandler);
view.addListener(mxEvent.TRANSLATE, this.updateHandler);
view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.updateHandler);
view.addListener(mxEvent.DOWN, this.updateHandler);
view.addListener(mxEvent.UP, this.updateHandler);
// Updates blue rectangle on scroll
mxEvent.addListener(this.source.container, 'scroll', this.updateHandler);
this.panHandler = (sender: any, evt: mxEventObject) => {
if (this.updateOnPan) {
(<Function>this.updateHandler)(sender, evt);
}
};
this.source.addListener(mxEvent.PAN, this.panHandler);
// Refreshes the graph in the outline after a refresh of the main graph
this.refreshHandler = (sender: any) => {
const outline = <mxGraph>this.outline;
outline.setStylesheet(this.source.getStylesheet());
outline.refresh();
};
this.source.addListener(mxEvent.REFRESH, this.refreshHandler);
// Creates the blue rectangle for the viewport
this.bounds = new mxRectangle(0, 0, 0, 0);
this.selectionBorder = new mxRectangleShape(
this.bounds,
null,
mxConstants.OUTLINE_COLOR,
mxConstants.OUTLINE_STROKEWIDTH
);
this.selectionBorder.dialect = this.outline.dialect;
this.selectionBorder.init(this.outline.getView().getOverlayPane());
const selectionBorderNode = <SVGGElement>this.selectionBorder.node;
// Handles event by catching the initial pointer start and then listening to the
// complete gesture on the event target. This is needed because all the events
// are routed via the initial element even if that element is removed from the
// DOM, which happens when we repaint the selection border and zoom handles.
const handler = (evt: Event) => {
const t = mxEvent.getSource(evt);
const redirect = (evt: Event) => {
const outline = <mxGraph>this.outline;
outline.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt));
};
var redirect2 = (evt: Event) => {
const outline = <mxGraph>this.outline;
mxEvent.removeGestureListeners(t, null, redirect, redirect2);
outline.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
};
const outline = <mxGraph>this.outline;
mxEvent.addGestureListeners(t, null, redirect, redirect2);
outline.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
};
mxEvent.addGestureListeners(this.selectionBorder.node, handler);
// Creates a small blue rectangle for sizing (sizer handle)
const sizer = this.sizer = this.createSizer();
const sizerNode = <SVGGElement>sizer.node;
sizer.init(this.outline.getView().getOverlayPane());
if (this.enabled) {
sizerNode.style.cursor = 'nwse-resize';
}
mxEvent.addGestureListeners(this.sizer.node, handler);
selectionBorderNode.style.display = this.showViewport ? '' : 'none';
sizerNode.style.display = selectionBorderNode.style.display;
selectionBorderNode.style.cursor = 'move';
this.update(false);
}
// TODO: Document me!! // TODO: Document me!!
sizer: mxRectangleShape | null=null; sizer: mxRectangleShape | null=null;
@ -32,7 +197,7 @@ class mxOutline {
bounds: mxRectangle | null=null; bounds: mxRectangle | null=null;
zoom: number | null=null; zoom: boolean=false;
startX: number | null=null; startX: number | null=null;
@ -56,17 +221,14 @@ class mxOutline {
* *
* Reference to the <mxGraph> that renders the outline. * Reference to the <mxGraph> that renders the outline.
*/ */
outline: mxGraph; outline: mxGraph | null=null;
/** /**
* Function: graphRenderHint * Function: graphRenderHint
* *
* Renderhint to be used for the outline graph. Default is faster. * Renderhint to be used for the outline graph. Default is exact.
*/ */
graphRenderHint: graphRenderHint: string = 'exact';
| mxConstants.RENDERING_HINT_EXACT
| mxConstants.RENDERING_HINT_FASTER
| mxConstants.RENDERING_HINT_FASTEST = mxConstants.RENDERING_HINT_FASTER;
/** /**
* Variable: enabled * Variable: enabled
@ -143,54 +305,6 @@ class mxOutline {
*/ */
suspended: boolean = false; suspended: boolean = false;
/**
* Class: mxOutline
*
* Implements an outline (aka overview) for a graph. Set <updateOnPan> to true
* to enable updates while the source graph is panning.
*
* Example:
*
* (code)
* let outline = new mxOutline(graph, div);
* (end)
*
* To move the graph to the top, left corner the following code can be used.
*
* (code)
* let scale = graph.view.scale;
* let bounds = graph.getGraphBounds();
* graph.view.setTranslate(-bounds.x / scale, -bounds.y / scale);
* (end)
*
* To toggle the suspended mode, the following can be used.
*
* (code)
* outline.suspended = !outln.suspended;
* if (!outline.suspended)
* {
* outline.update(true);
* }
* (end)
*
* Constructor: mxOutline
*
* Constructs a new outline for the specified graph inside the given
* container.
*
* Parameters:
*
* source - <mxGraph> to create the outline for.
* container - DOM node that will contain the outline.
*/
constructor(source: mxGraph, container: HTMLElement | null = null) {
this.source = source;
if (container != null) {
this.init(container);
}
}
/** /**
* Function: createGraph * Function: createGraph
* *
@ -208,118 +322,6 @@ class mxOutline {
return graph; return graph;
} }
/**
* Function: init
*
* Initializes the outline inside the given container.
*/
init(container: HTMLElement) {
this.outline = this.createGraph(container);
// Do not repaint when suspended
const outlineGraphModelChanged = this.outline.graphModelChanged;
this.outline.graphModelChanged = mxUtils.bind(this, changes => {
if (!this.suspended && this.outline != null) {
outlineGraphModelChanged.apply(this.outline, [changes]);
}
});
// Enable faster painting in SVG
const node = <SVGElement>this.outline.getView().getCanvas().parentNode;
node.setAttribute('shape-rendering', 'optimizeSpeed');
node.setAttribute('image-rendering', 'optimizeSpeed');
// Hides cursors and labels
this.outline.labelsVisible = this.labelsVisible;
this.outline.setEnabled(false);
this.updateHandler = (sender, evt) => {
if (!this.suspended && !this.active) {
this.update();
}
};
// Updates the scale of the outline after a change of the main graph
this.source.getModel().addListener(mxEvent.CHANGE, this.updateHandler);
this.outline.addMouseListener(this);
// Adds listeners to keep the outline in sync with the source graph
const view = this.source.getView();
view.addListener(mxEvent.SCALE, this.updateHandler);
view.addListener(mxEvent.TRANSLATE, this.updateHandler);
view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.updateHandler);
view.addListener(mxEvent.DOWN, this.updateHandler);
view.addListener(mxEvent.UP, this.updateHandler);
// Updates blue rectangle on scroll
mxEvent.addListener(this.source.container, 'scroll', this.updateHandler);
this.panHandler = (sender, evt) => {
if (this.updateOnPan) {
this.updateHandler.apply(this, [sender, evt]);
}
};
this.source.addListener(mxEvent.PAN, this.panHandler);
// Refreshes the graph in the outline after a refresh of the main graph
this.refreshHandler = sender => {
this.outline.setStylesheet(this.source.getStylesheet());
this.outline.refresh();
};
this.source.addListener(mxEvent.REFRESH, this.refreshHandler);
// Creates the blue rectangle for the viewport
this.bounds = new mxRectangle(0, 0, 0, 0);
this.selectionBorder = new mxRectangleShape(
this.bounds,
null,
mxConstants.OUTLINE_COLOR,
mxConstants.OUTLINE_STROKEWIDTH
);
this.selectionBorder.dialect = this.outline.dialect;
this.selectionBorder.init(this.outline.getView().getOverlayPane());
// Handles event by catching the initial pointer start and then listening to the
// complete gesture on the event target. This is needed because all the events
// are routed via the initial element even if that element is removed from the
// DOM, which happens when we repaint the selection border and zoom handles.
const handler = mxUtils.bind(this, evt => {
const t = mxEvent.getSource(evt);
const redirect = mxUtils.bind(this, evt => {
this.outline.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt));
});
var redirect2 = mxUtils.bind(this, evt => {
mxEvent.removeGestureListeners(t, null, redirect, redirect2);
this.outline.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt));
});
mxEvent.addGestureListeners(t, null, redirect, redirect2);
this.outline.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt));
});
mxEvent.addGestureListeners(this.selectionBorder.node, handler);
// Creates a small blue rectangle for sizing (sizer handle)
this.sizer = this.createSizer();
this.sizer.init(this.outline.getView().getOverlayPane());
if (this.enabled) {
this.sizer.node.style.cursor = 'nwse-resize';
}
mxEvent.addGestureListeners(this.sizer.node, handler);
this.selectionBorder.node.style.display = this.showViewport ? '' : 'none';
this.sizer.node.style.display = this.selectionBorder.node.style.display;
this.selectionBorder.node.style.cursor = 'move';
this.update(false);
}
/** /**
* Function: isEnabled * Function: isEnabled
* *
@ -355,6 +357,7 @@ class mxOutline {
* value - Boolean that specifies the new enabled state. * value - Boolean that specifies the new enabled state.
*/ */
setZoomEnabled(value: boolean): void { setZoomEnabled(value: boolean): void {
// @ts-ignore
this.sizer.node.style.visibility = value ? 'visible' : 'hidden'; this.sizer.node.style.visibility = value ? 'visible' : 'hidden';
} }
@ -373,12 +376,13 @@ class mxOutline {
* Creates the shape used as the sizer. * Creates the shape used as the sizer.
*/ */
createSizer(): mxRectangleShape { createSizer(): mxRectangleShape {
const outline = <mxGraph>this.outline;
if (this.sizerImage != null) { if (this.sizerImage != null) {
const sizer = new mxImageShape( const sizer = new mxImageShape(
new mxRectangle(0, 0, this.sizerImage.width, this.sizerImage.height), new mxRectangle(0, 0, this.sizerImage.width, this.sizerImage.height),
this.sizerImage.src this.sizerImage.src
); );
sizer.dialect = this.outline.dialect; sizer.dialect = outline.dialect;
return sizer; return sizer;
} }
@ -387,7 +391,7 @@ class mxOutline {
mxConstants.OUTLINE_HANDLE_FILLCOLOR, mxConstants.OUTLINE_HANDLE_FILLCOLOR,
mxConstants.OUTLINE_HANDLE_STROKECOLOR mxConstants.OUTLINE_HANDLE_STROKECOLOR
); );
sizer.dialect = this.outline.dialect; sizer.dialect = outline.dialect;
return sizer; return sizer;
} }
@ -400,8 +404,8 @@ class mxOutline {
return new mxRectangle( return new mxRectangle(
0, 0,
0, 0,
this.source.container.scrollWidth, (<HTMLElement>this.source.container).scrollWidth,
this.source.container.scrollHeight (<HTMLElement>this.source.container).scrollHeight
); );
} }
@ -410,7 +414,7 @@ class mxOutline {
* *
* Returns the offset for drawing the outline graph. * Returns the offset for drawing the outline graph.
*/ */
getOutlineOffset(scale) { getOutlineOffset(scale: number): mxPoint | null { // TODO: Should number -> mxPoint?
return null; return null;
} }
@ -533,7 +537,8 @@ class mxOutline {
this.bounds.y += this.bounds.y +=
(this.source.container.scrollTop * navView.scale) / scale; (this.source.container.scrollTop * navView.scale) / scale;
let b = this.selectionBorder.bounds; const selectionBorder = <mxRectangleShape>this.selectionBorder;
let b = <mxRectangle>selectionBorder.bounds;
if ( if (
b.x !== this.bounds.x || b.x !== this.bounds.x ||
@ -541,12 +546,13 @@ class mxOutline {
b.width !== this.bounds.width || b.width !== this.bounds.width ||
b.height !== this.bounds.height b.height !== this.bounds.height
) { ) {
this.selectionBorder.bounds = this.bounds; selectionBorder.bounds = this.bounds;
this.selectionBorder.redraw(); selectionBorder.redraw();
} }
// Updates the bounds of the zoom handle at the bottom right // Updates the bounds of the zoom handle at the bottom right
b = this.sizer.bounds; const sizer = <mxRectangleShape>this.sizer;
b = <mxRectangle>sizer.bounds;
const b2 = new mxRectangle( const b2 = new mxRectangle(
this.bounds.x + this.bounds.width - b.width / 2, this.bounds.x + this.bounds.width - b.width / 2,
this.bounds.y + this.bounds.height - b.height / 2, this.bounds.y + this.bounds.height - b.height / 2,
@ -560,11 +566,11 @@ class mxOutline {
b.width !== b2.width || b.width !== b2.width ||
b.height !== b2.height b.height !== b2.height
) { ) {
this.sizer.bounds = b2; sizer.bounds = b2;
// Avoids update of visibility in redraw for VML // Avoids update of visibility in redraw for VML
if (this.sizer.node.style.visibility !== 'hidden') { if ((<SVGGElement>sizer.node).style.visibility !== 'hidden') {
this.sizer.redraw(); sizer.redraw();
} }
} }
@ -580,7 +586,7 @@ class mxOutline {
* *
* Handles the event by starting a translation or zoom. * Handles the event by starting a translation or zoom.
*/ */
mouseDown(sender, me) { mouseDown(sender: any, me: mxMouseEvent) {
if (this.enabled && this.showViewport) { if (this.enabled && this.showViewport) {
const tol = !mxEvent.isMouseEvent(me.getEvent()) const tol = !mxEvent.isMouseEvent(me.getEvent())
? this.source.tolerance ? this.source.tolerance
@ -600,13 +606,14 @@ class mxOutline {
this.startX = me.getX(); this.startX = me.getX();
this.startY = me.getY(); this.startY = me.getY();
this.active = true; this.active = true;
const sourceContainer = <HTMLElement>this.source.container;
if ( if (
this.source.useScrollbarsForPanning && this.source.useScrollbarsForPanning &&
mxUtils.hasScrollbars(this.source.container) mxUtils.hasScrollbars(this.source.container)
) { ) {
this.dx0 = this.source.container.scrollLeft; this.dx0 = sourceContainer.scrollLeft;
this.dy0 = this.source.container.scrollTop; this.dy0 = sourceContainer.scrollTop;
} else { } else {
this.dx0 = 0; this.dx0 = 0;
this.dy0 = 0; this.dy0 = 0;
@ -622,10 +629,18 @@ class mxOutline {
* Handles the event by previewing the viewrect in <graph> and updating the * Handles the event by previewing the viewrect in <graph> and updating the
* rectangle that represents the viewrect in the outline. * rectangle that represents the viewrect in the outline.
*/ */
mouseMove(sender, me) { mouseMove(sender: any, me: mxMouseEvent) {
if (this.active) { if (this.active) {
this.selectionBorder.node.style.display = this.showViewport ? '' : 'none'; const myBounds = <mxRectangle>this.bounds;
this.sizer.node.style.display = this.selectionBorder.node.style.display; const sizer = <mxRectangleShape>this.sizer;
const sizerNode = <SVGGElement>sizer.node;
const selectionBorder = <mxRectangleShape>this.selectionBorder;
const selectionBorderNode = <SVGGElement>selectionBorder.node;
const source = <mxGraph>this.source;
const outline = <mxGraph>this.outline;
selectionBorderNode.style.display = this.showViewport ? '' : 'none';
sizerNode.style.display = selectionBorderNode.style.display;
const delta = this.getTranslateForEvent(me); const delta = this.getTranslateForEvent(me);
let dx = delta.x; let dx = delta.x;
@ -634,38 +649,39 @@ class mxOutline {
if (!this.zoom) { if (!this.zoom) {
// Previews the panning on the source graph // Previews the panning on the source graph
const { scale } = this.outline.getView(); const { scale } = outline.getView();
bounds = new mxRectangle( bounds = new mxRectangle(
this.bounds.x + dx, myBounds.x + dx,
this.bounds.y + dy, myBounds.y + dy,
this.bounds.width, myBounds.width,
this.bounds.height myBounds.height
); );
this.selectionBorder.bounds = bounds; selectionBorder.bounds = bounds;
this.selectionBorder.redraw(); selectionBorder.redraw();
dx /= scale; dx /= scale;
dx *= this.source.getView().scale; dx *= source.getView().scale;
dy /= scale; dy /= scale;
dy *= this.source.getView().scale; dy *= source.getView().scale;
this.source.panGraph(-dx - this.dx0, -dy - this.dy0); source.panGraph(-dx - <number>this.dx0, -dy - <number>this.dy0);
} else { } else {
// Does *not* preview zooming on the source graph // Does *not* preview zooming on the source graph
const { container } = this.source; const { container } = <mxGraph>this.source;
// @ts-ignore
const viewRatio = container.clientWidth / container.clientHeight; const viewRatio = container.clientWidth / container.clientHeight;
dy = dx / viewRatio; dy = dx / viewRatio;
bounds = new mxRectangle( bounds = new mxRectangle(
this.bounds.x, myBounds.x,
this.bounds.y, myBounds.y,
Math.max(1, this.bounds.width + dx), Math.max(1, myBounds.width + dx),
Math.max(1, this.bounds.height + dy) Math.max(1, myBounds.height + dy)
); );
this.selectionBorder.bounds = bounds; selectionBorder.bounds = bounds;
this.selectionBorder.redraw(); selectionBorder.redraw();
} }
// Updates the zoom handle // Updates the zoom handle
const b = this.sizer.bounds; const b = <mxRectangle>sizer.bounds;
this.sizer.bounds = new mxRectangle( sizer.bounds = new mxRectangle(
bounds.x + bounds.width - b.width / 2, bounds.x + bounds.width - b.width / 2,
bounds.y + bounds.height - b.height / 2, bounds.y + bounds.height - b.height / 2,
b.width, b.width,
@ -673,10 +689,9 @@ class mxOutline {
); );
// Avoids update of visibility in redraw for VML // Avoids update of visibility in redraw for VML
if (this.sizer.node.style.visibility !== 'hidden') { if (sizerNode.style.visibility !== 'hidden') {
this.sizer.redraw(); sizer.redraw();
} }
me.consume(); me.consume();
} }
} }
@ -703,8 +718,8 @@ class mxOutline {
* }; * };
* (end) * (end)
*/ */
getTranslateForEvent(me) { getTranslateForEvent(me: mxMouseEvent) {
return new mxPoint(me.getX() - this.startX, me.getY() - this.startY); return new mxPoint(me.getX() - <number>this.startX, me.getY() - <number>this.startY);
} }
/** /**
@ -712,31 +727,34 @@ class mxOutline {
* *
* Handles the event by applying the translation or zoom to <graph>. * Handles the event by applying the translation or zoom to <graph>.
*/ */
mouseUp(sender, me) { mouseUp(sender: any, me: mxMouseEvent) {
if (this.active) { if (this.active) {
const delta = this.getTranslateForEvent(me); const delta = this.getTranslateForEvent(me);
let dx = delta.x; let dx = delta.x;
let dy = delta.y; let dy = delta.y;
const source = <mxGraph>this.source;
const outline = <mxGraph>this.outline;
const selectionBorder = <mxRectangleShape>this.selectionBorder;
if (Math.abs(dx) > 0 || Math.abs(dy) > 0) { if (Math.abs(dx) > 0 || Math.abs(dy) > 0) {
if (!this.zoom) { if (!this.zoom) {
// Applies the new translation if the source // Applies the new translation if the source
// has no scrollbars // has no scrollbars
if ( if (
!this.source.useScrollbarsForPanning || !source.useScrollbarsForPanning ||
!mxUtils.hasScrollbars(this.source.container) !mxUtils.hasScrollbars(source.container)
) { ) {
this.source.panGraph(0, 0); source.panGraph(0, 0);
dx /= this.outline.getView().scale; dx /= outline.getView().scale;
dy /= this.outline.getView().scale; dy /= outline.getView().scale;
const t = this.source.getView().translate; const t = source.getView().translate;
this.source.getView().setTranslate(t.x - dx, t.y - dy); source.getView().setTranslate(t.x - dx, t.y - dy);
} }
} else { } else {
// Applies the new zoom // Applies the new zoom
const w = this.selectionBorder.bounds.width; const w = (<mxRectangle>selectionBorder.bounds).width;
const { scale } = this.source.getView(); const { scale } = source.getView();
this.source.zoomTo( source.zoomTo(
Math.max(this.minScale, scale - (dx * scale) / w), Math.max(this.minScale, scale - (dx * scale) / w),
false false
); );
@ -768,6 +786,7 @@ class mxOutline {
'scroll', 'scroll',
this.updateHandler this.updateHandler
); );
// @ts-ignore
this.source = null; this.source = null;
} }

View File

@ -1,27 +0,0 @@
class ExampleBase {
props: object;
constructor(props: object) {
this.props = props;
}
appendToElement(element: HTMLElement): HTMLElement {
const html: string = this.getHTML();
const cont: HTMLElement =
document.createElement('div');
cont.innerHTML = html;
this.afterHTMLSet()
element.appendChild(cont);
return cont;
}
getHTML(): void {
throw new Error("Not implemented");
}
afterHTMLSet(): void {
throw new Error("Not implemented");
}
}
export default ExampleBase;

View File

@ -0,0 +1,272 @@
/**
* Copyright (c) 2006-2013, JGraph Ltd
* Converted to ES9 syntax/React by David Morrissey 2021
*/
import React from 'react';
import mxEvent from '../../mxgraph/util/mxEvent';
import mxGraph from '../../mxgraph/view/mxGraph';
import mxRubberband from '../../mxgraph/handler/mxRubberband';
import mxRectangle from '../../mxgraph/util/mxRectangle';
import mxUtils from '../../mxgraph/util/mxUtils';
import mxPoint from '../../mxgraph/util/mxPoint';
class ExtendCanvas extends React.Component {
constructor(props) {
super(props);
}
render() {
// A container for the graph
return (
<>
<h1>Extend canvas</h1>
This example demonstrates implementing an infinite canvas with
scrollbars.
<div
ref={el => {
this.el = el;
}}
style={{
position: 'relative',
overflow: 'auto',
height: '241px',
background: "url('editors/images/grid.gif')",
cursor: 'default',
}}
/>
</>
);
}
componentDidMount() {
// Disables the built-in context menu
mxEvent.disableContextMenu(this.el);
/**
* Specifies the size of the size for "tiles" to be used for a graph with
* scrollbars but no visible background page. A good value is large
* enough to reduce the number of repaints that is caused for auto-
* translation, which depends on this value, and small enough to give
* a small empty buffer around the graph. Default is 400x400.
*/
const scrollTileSize = new mxRectangle(0, 0, 400, 400);
class MyCustomGraph extends mxGraph {
/**
* Returns the padding for pages in page view with scrollbars.
*/
getPagePadding() {
return new mxPoint(
Math.max(0, Math.round(this.container.offsetWidth - 34)),
Math.max(0, Math.round(this.container.offsetHeight - 34))
);
}
/**
* Returns the size of the page format scaled with the page size.
*/
getPageSize() {
return this.pageVisible
? new mxRectangle(
0,
0,
this.pageFormat.width * this.pageScale,
this.pageFormat.height * this.pageScale
)
: scrollTileSize;
}
/**
* Returns a rectangle describing the position and count of the
* background pages, where x and y are the position of the top,
* left page and width and height are the vertical and horizontal
* page count.
*/
getPageLayout() {
const size = this.pageVisible
? this.getPageSize()
: scrollTileSize;
const bounds = this.getGraphBounds();
if (bounds.width === 0 || bounds.height === 0) {
return new mxRectangle(0, 0, 1, 1);
}
// Computes untransformed graph bounds
const x = Math.ceil(bounds.x / this.view.scale - this.view.translate.x);
const y = Math.ceil(bounds.y / this.view.scale - this.view.translate.y);
const w = Math.floor(bounds.width / this.view.scale);
const h = Math.floor(bounds.height / this.view.scale);
const x0 = Math.floor(x / size.width);
const y0 = Math.floor(y / size.height);
const w0 = Math.ceil((x + w) / size.width) - x0;
const h0 = Math.ceil((y + h) / size.height) - y0;
return new mxRectangle(x0, y0, w0, h0);
}
getPreferredPageSize(bounds, width, height) {
const pages = this.getPageLayout();
const size = this.getPageSize();
return new mxRectangle(
0,
0,
pages.width * size.width,
pages.height * size.height
);
}
sizeDidChange() {
if (this.container != null && mxUtils.hasScrollbars(this.container)) {
const pages = this.getPageLayout();
const pad = this.getPagePadding();
const size = this.getPageSize();
// Updates the minimum graph size
const minw = Math.ceil(
(2 * pad.x) / this.view.scale + pages.width * size.width
);
const minh = Math.ceil(
(2 * pad.y) / this.view.scale + pages.height * size.height
);
const min = this.minimumGraphSize;
// LATER: Fix flicker of scrollbar size in IE quirks mode
// after delayed call in window.resize event handler
if (min == null || min.width !== minw || min.height !== minh) {
this.minimumGraphSize = new mxRectangle(0, 0, minw, minh);
}
// Updates auto-translate to include padding and graph size
const dx = pad.x / this.view.scale - pages.x * size.width;
const dy = pad.y / this.view.scale - pages.y * size.height;
if (
!this.autoTranslate &&
(this.view.translate.x !== dx || this.view.translate.y !== dy)
) {
this.autoTranslate = true;
this.view.x0 = pages.x;
this.view.y0 = pages.y;
// NOTE: THIS INVOKES THIS METHOD AGAIN. UNFORTUNATELY THERE IS NO WAY AROUND THIS SINCE THE
// BOUNDS ARE KNOWN AFTER THE VALIDATION AND SETTING THE TRANSLATE TRIGGERS A REVALIDATION.
// SHOULD MOVE TRANSLATE/SCALE TO VIEW.
const tx = this.view.translate.x;
const ty = this.view.translate.y;
this.view.setTranslate(dx, dy);
this.container.scrollLeft += (dx - tx) * this.view.scale;
this.container.scrollTop += (dy - ty) * this.view.scale;
this.autoTranslate = false;
return;
}
super.sizeDidChange();
}
}
}
// Creates the graph inside the given container
const graph = this.graph = new MyCustomGraph(this.el);
graph.panningHandler.ignoreCell = true;
graph.setPanning(true);
// Fits the number of background pages to the graph
graph.view.getBackgroundPageBounds = function() {
const layout = this.graph.getPageLayout();
const page = this.graph.getPageSize();
return new mxRectangle(
this.scale * (this.translate.x + layout.x * page.width),
this.scale * (this.translate.y + layout.y * page.height),
this.scale * layout.width * page.width,
this.scale * layout.height * page.height
);
};
/**
* Guesses autoTranslate to avoid another repaint (see below).
* Works if only the scale of the graph changes or if pages
* are visible and the visible pages do not change.
*/
const graphViewValidate = graph.view.validate;
graph.view.validate = function() {
if (
this.graph.container != null &&
mxUtils.hasScrollbars(this.graph.container)
) {
const pad = this.graph.getPagePadding();
const size = this.graph.getPageSize();
// Updating scrollbars here causes flickering in quirks and is not needed
// if zoom method is always used to set the current scale on the graph.
const tx = this.translate.x;
const ty = this.translate.y;
this.translate.x = pad.x / this.scale - (this.x0 || 0) * size.width;
this.translate.y = pad.y / this.scale - (this.y0 || 0) * size.height;
}
graphViewValidate.apply(this, arguments);
};
// Enables rubberband selection
new mxRubberband(graph);
// Gets the default parent for inserting new cells. This
// is normally the first child of the root (ie. layer 0).
const parent = graph.getDefaultParent();
// Adds cells to the model in a single step
graph.batchUpdate(() => {
const v1 = graph.insertVertex({
parent,
value: 'Hello,',
position: [20, 20],
size: [80, 30],
});
const v2 = graph.insertVertex({
parent,
value: 'World!',
position: [200, 150],
size: [80, 30],
});
const e1 = graph.insertEdge({
parent,
source: v1,
target: v2,
});
});
// Sets initial scrollbar positions
window.setTimeout(() => {
const bounds = graph.getGraphBounds();
const width = Math.max(
bounds.width,
scrollTileSize.width * graph.view.scale
);
const height = Math.max(
bounds.height,
scrollTileSize.height * graph.view.scale
);
graph.container.scrollTop = Math.floor(
Math.max(
0,
bounds.y - Math.max(20, (graph.container.clientHeight - height) / 4)
)
);
graph.container.scrollLeft = Math.floor(
Math.max(
0,
bounds.x - Math.max(0, (graph.container.clientWidth - width) / 2)
)
);
}, 0);
}
}
export default ExtendCanvas;

View File

@ -1,282 +0,0 @@
/**
* Copyright (c) 2006-2013, JGraph Ltd
* Converted to ES9 syntax/React by David Morrissey 2021
*/
import mxEvent from '../../mxgraph/util/event/mxEvent';
import mxGraph from '../../mxgraph/view/graph/mxGraph';
import mxGraphView from '../../mxgraph/view/graph/mxGraphView'
import mxRubberband from '../../mxgraph/handler/mxRubberband';
import mxRectangle from '../../mxgraph/util/datatypes/mxRectangle';
import mxUtils from '../../mxgraph/util/mxUtils';
import mxPoint from '../../mxgraph/util/datatypes/mxPoint';
import mxCell from '../../mxgraph/view/cell/mxCell';
import ExampleBase from "./ExampleBase";
/**
* Specifies the size of the size for "tiles" to be used for a graph with
* scrollbars but no visible background page. A good value is large
* enough to reduce the number of repaints that is caused for auto-
* translation, which depends on this value, and small enough to give
* a small empty buffer around the graph. Default is 400x400.
*/
const scrollTileSize = new mxRectangle(0, 0, 400, 400);
class ExtendCanvas extends ExampleBase {
constructor(props) {
super(props);
}
getHTML() {
// A container for the graph
return `
<h1>Extend canvas</h1>
This example demonstrates implementing an infinite canvas with
scrollbars.
<div id="el"
style="position: relative;
overflow: auto;
height: 241px;
background: url('editors/images/grid.gif');
cursor: default"
/>
`;
}
afterHTMLSet(): void {
// Executed after the HTML has been set
const el = document.getElementById("el");
el.id = '';
// Disables the built-in context menu
mxEvent.disableContextMenu(el);
// Creates the graph inside the given container
const graph: MyCustomGraph = new MyCustomGraph(el);
graph.panningHandler.ignoreCell = true;
graph.setPanning(true);
// Enables rubberband selection
new mxRubberband(graph);
// Gets the default parent for inserting new cells. This
// is normally the first child of the root (ie. layer 0).
const parent = graph.getDefaultParent();
// Adds cells to the model in a single step
graph.batchUpdate(() => {
const v1: mxCell = graph.insertVertex({
parent,
value: 'Hello,',
position: [20, 20],
size: [80, 30],
});
const v2: mxCell = graph.insertVertex({
parent,
value: 'World!',
position: [200, 150],
size: [80, 30],
});
const e1: mxCell = graph.insertEdge({
parent,
source: v1,
target: v2,
});
});
// Sets initial scrollbar positions
window.setTimeout(() => {
const bounds: mxRectangle = graph.getGraphBounds();
const width: number = Math.max(
bounds.width,
scrollTileSize.width * graph.view.scale
);
const height: number = Math.max(
bounds.height,
scrollTileSize.height * graph.view.scale
);
graph.container.scrollTop = Math.floor(
Math.max(
0,
bounds.y - Math.max(20, (graph.container.clientHeight - height) / 4)
)
);
graph.container.scrollLeft = Math.floor(
Math.max(
0,
bounds.x - Math.max(0, (graph.container.clientWidth - width) / 2)
)
);
}, 0);
}
}
class MyCustomGraph extends mxGraph {
autoTranslate: boolean;
createGraphView(): mxGraphView {
return new MyCustomGraphView(this);
}
/**
* Returns the padding for pages in page view with scrollbars.
*/
getPagePadding(): mxPoint {
return new mxPoint(
Math.max(0, Math.round(this.container.offsetWidth - 34)),
Math.max(0, Math.round(this.container.offsetHeight - 34))
);
}
/**
* Returns the size of the page format scaled with the page size.
*/
getPageSize(): mxRectangle {
return this.pageVisible
? new mxRectangle(
0,
0,
this.pageFormat.width * this.pageScale,
this.pageFormat.height * this.pageScale
)
: scrollTileSize;
}
/**
* Returns a rectangle describing the position and count of the
* background pages, where x and y are the position of the top,
* left page and width and height are the vertical and horizontal
* page count.
*/
getPageLayout(): mxRectangle {
const size: mxRectangle = this.pageVisible
? this.getPageSize()
: scrollTileSize;
const bounds = this.getGraphBounds();
if (bounds.width === 0 || bounds.height === 0) {
return new mxRectangle(0, 0, 1, 1);
}
// Computes untransformed graph bounds
const x: number = Math.ceil(bounds.x / this.view.scale - this.view.translate.x);
const y: number = Math.ceil(bounds.y / this.view.scale - this.view.translate.y);
const w: number = Math.floor(bounds.width / this.view.scale);
const h: number = Math.floor(bounds.height / this.view.scale);
const x0: number = Math.floor(x / size.width);
const y0: number = Math.floor(y / size.height);
const w0: number = Math.ceil((x + w) / size.width) - x0;
const h0: number = Math.ceil((y + h) / size.height) - y0;
return new mxRectangle(x0, y0, w0, h0);
}
getPreferredPageSize(bounds, width, height): mxRectangle {
const pages: mxRectangle = this.getPageLayout();
const size: mxRectangle = this.getPageSize();
return new mxRectangle(
0,
0,
pages.width * size.width,
pages.height * size.height
);
}
sizeDidChange(): mxRectangle {
if (this.container != null && mxUtils.hasScrollbars(this.container)) {
const pages: mxRectangle = this.getPageLayout();
const pad: mxPoint = this.getPagePadding();
const size: mxRectangle = this.getPageSize();
// Updates the minimum graph size
const minw: number = Math.ceil(
(2 * pad.x) / this.view.scale + pages.width * size.width
);
const minh: number = Math.ceil(
(2 * pad.y) / this.view.scale + pages.height * size.height
);
const min: number = this.minimumGraphSize;
// LATER: Fix flicker of scrollbar size in IE quirks mode
// after delayed call in window.resize event handler
if (min == null || min.width !== minw || min.height !== minh) {
this.minimumGraphSize = new mxRectangle(0, 0, minw, minh);
}
// Updates auto-translate to include padding and graph size
const dx: number = pad.x / this.view.scale - pages.x * size.width;
const dy: number = pad.y / this.view.scale - pages.y * size.height;
if (
!this.autoTranslate &&
(this.view.translate.x !== dx || this.view.translate.y !== dy)
) {
this.autoTranslate = true;
this.view.x0 = pages.x;
this.view.y0 = pages.y;
// NOTE: THIS INVOKES THIS METHOD AGAIN. UNFORTUNATELY THERE IS NO WAY AROUND THIS SINCE THE
// BOUNDS ARE KNOWN AFTER THE VALIDATION AND SETTING THE TRANSLATE TRIGGERS A REVALIDATION.
// SHOULD MOVE TRANSLATE/SCALE TO VIEW.
const tx: number = this.view.translate.x;
const ty: number = this.view.translate.y;
this.view.setTranslate(dx, dy);
this.container.scrollLeft += (dx - tx) * this.view.scale;
this.container.scrollTop += (dy - ty) * this.view.scale;
this.autoTranslate = false;
return;
}
super.sizeDidChange();
}
}
}
class MyCustomGraphView extends mxGraphView {
/**
* Fits the number of background pages to the graph
*/
getBackgroundPageBounds(): mxRectangle {
const layout: mxRectangle = this.graph.getPageLayout();
const page: mxRectangle = this.graph.getPageSize();
return new mxRectangle(
this.scale * (this.translate.x + layout.x * page.width),
this.scale * (this.translate.y + layout.y * page.height),
this.scale * layout.width * page.width,
this.scale * layout.height * page.height
);
}
/**
* Guesses autoTranslate to avoid another repaint (see below).
* Works if only the scale of the graph changes or if pages
* are visible and the visible pages do not change.
*/
validate(): void {
if (
this.graph.container != null &&
mxUtils.hasScrollbars(this.graph.container)
) {
const pad: mxPoint = this.graph.getPagePadding();
const size: mxRectangle = this.graph.getPageSize();
// Updating scrollbars here causes flickering in quirks and is not needed
// if zoom method is always used to set the current scale on the graph.
//const tx = this.translate.x;
//const ty = this.translate.y;
this.translate.x = pad.x / this.scale - (this.x0 || 0) * size.width;
this.translate.y = pad.y / this.scale - (this.y0 || 0) * size.height;
}
super.validate();
};
}
export default ExtendCanvas;