create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
979 lines (977 loc) • 39.9 kB
JavaScript
/*
* Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved.
*/
/*
* This is an extension and not part of the main GoJS library.
* The source code for this is at extensionsJSM/DrawCommandHandler.ts.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/
/**
* This CommandHandler class allows the user to position selected Parts in a diagram
* relative to the first part selected, in addition to overriding the doKeyDown method
* of the CommandHandler for handling the arrow keys in additional manners.
*
* Typical usage:
* ```js
* new go.Diagram("myDiagramDiv",
* {
* commandHandler: new DrawCommandHandler(),
* . . .
* }
* )
* ```
* or:
* ```js
* myDiagram.commandHandler = new DrawCommandHandler();
* ```
*
* If you want to experiment with this extension, try the <a href="../../samples/DrawCommandHandler.html">Drawing Commands</a> sample.
*
* New in version 3.1 this adds a command to save the model as a text file in the user's local file system,
* typically in their Downloads folder, {@link saveLocalFile}.
* And it adds a method for loading a File: {@link loadLocalFile}.
*
* There are two optional properties that can be used for calling {@link loadLocalFile}:
* {@link localFileInput} and {@link localFileDropElement}.
* The former may be a file type HTMLInputElement that you have on your page;
* the latter may be an HTMLElement, or even the whole document.body, where the user may drag-and-drop a saved file.
*
* The default file type for files is controlled by {@link localFileType}.
* @category Extension
*/
class DrawCommandHandler extends go.CommandHandler {
constructor(init) {
super();
this._arrowKeyBehavior = 'move';
this._pasteOffset = new go.Point(10, 10);
this._lastPasteOffset = new go.Point(0, 0);
this._localFileType = 'gojs';
this._localFileDropElement = null;
this._preventPropagation = (e) => {
e.stopPropagation();
e.preventDefault();
};
this._handleDrop = (e) => {
var _a;
this._preventPropagation(e);
const files = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files;
if (files && files.length > 0) {
this.diagram.focus();
this.loadLocalFile(files[0], this.loader);
}
};
this._localFileInput = null;
this._handleFileInputChange = (e) => {
var _a;
const files = (_a = this._localFileInput) === null || _a === void 0 ? void 0 : _a.files;
if (files && files.length > 0) {
this.diagram.focus();
this.loadLocalFile(files[0], this.loader);
}
};
this._loader = null;
if (init)
Object.assign(this, init);
}
/**
* Gets or sets the arrow key behavior. Possible values are "move", "select", "scroll", "tree", "none", and "default".
*
* The default value is "move".
* Set this property to "default" in order to make use of the additional commands in this class
* without affecting the arrow key behaviors.
*
* Note that this functionality is different from the focus navigation behavior of the {@link CommandHandler}
* that was added in version 3.1 and enabled by the {@link CommandHandler.isFocusEnabled} property.
* In this DrawCommandHandler the arrow keys for the "move", "select" or "tree" behaviors
* depend on and modify the {@link Diagram.selection}. The built-in focus navigation is completely independent
* of the selection mechanism.
*/
get arrowKeyBehavior() {
return this._arrowKeyBehavior;
}
set arrowKeyBehavior(val) {
if (val !== 'move' &&
val !== 'select' &&
val !== 'scroll' &&
val !== 'none' &&
val !== 'tree') {
throw new Error('DrawCommandHandler.arrowKeyBehavior must be either "move", "select", "scroll", "tree", or "none", not: ' +
val);
}
this._arrowKeyBehavior = val;
}
/**
* Gets or sets the offset at which each repeated {@link pasteSelection} puts the new copied parts from the clipboard.
*/
get pasteOffset() {
return this._pasteOffset;
}
set pasteOffset(val) {
if (!(val instanceof go.Point))
throw new Error('DrawCommandHandler.pasteOffset must be a Point, not: ' + val);
this._pasteOffset.set(val);
}
/**
* This controls whether or not the user can invoke the {@link alignLeft}, {@link alignRight},
* {@link alignTop}, {@link alignBottom}, {@link alignCenterX}, {@link alignCenterY} commands.
* @returns This returns true:
* if the diagram is not {@link go.Diagram.isReadOnly},
* if the model is not {@link go.Model.isReadOnly}, and
* if there are at least two selected {@link go.Part}s.
*/
canAlignSelection() {
const diagram = this.diagram;
if (diagram.isReadOnly || diagram.isModelReadOnly)
return false;
if (diagram.selection.count < 2)
return false;
return true;
}
/**
* Aligns selected parts along the left-most edge of the left-most part.
*/
alignLeft() {
const diagram = this.diagram;
diagram.startTransaction('aligning left');
let minPosition = Infinity;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
minPosition = Math.min(current.position.x, minPosition);
});
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
current.moveTo(minPosition, current.position.y);
});
diagram.commitTransaction('aligning left');
}
/**
* Aligns selected parts at the right-most edge of the right-most part.
*/
alignRight() {
const diagram = this.diagram;
diagram.startTransaction('aligning right');
let maxPosition = -Infinity;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
const rightSideLoc = current.actualBounds.x + current.actualBounds.width;
maxPosition = Math.max(rightSideLoc, maxPosition);
});
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
current.moveTo(maxPosition - current.actualBounds.width, current.position.y);
});
diagram.commitTransaction('aligning right');
}
/**
* Aligns selected parts at the top-most edge of the top-most part.
*/
alignTop() {
const diagram = this.diagram;
diagram.startTransaction('alignTop');
let minPosition = Infinity;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
minPosition = Math.min(current.position.y, minPosition);
});
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
current.moveTo(current.position.x, minPosition);
});
diagram.commitTransaction('alignTop');
}
/**
* Aligns selected parts at the bottom-most edge of the bottom-most part.
*/
alignBottom() {
const diagram = this.diagram;
diagram.startTransaction('aligning bottom');
let maxPosition = -Infinity;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
const bottomSideLoc = current.actualBounds.y + current.actualBounds.height;
maxPosition = Math.max(bottomSideLoc, maxPosition);
});
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
current.moveTo(current.actualBounds.x, maxPosition - current.actualBounds.height);
});
diagram.commitTransaction('aligning bottom');
}
/**
* Aligns selected parts at the x-value of the center point of the first selected part.
*/
alignCenterX() {
const diagram = this.diagram;
const firstSelection = diagram.selection.first();
if (!firstSelection)
return;
diagram.startTransaction('aligning Center X');
const centerX = firstSelection.actualBounds.x + firstSelection.actualBounds.width / 2;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
current.moveTo(centerX - current.actualBounds.width / 2, current.actualBounds.y);
});
diagram.commitTransaction('aligning Center X');
}
/**
* Aligns selected parts at the y-value of the center point of the first selected part.
*/
alignCenterY() {
const diagram = this.diagram;
const firstSelection = diagram.selection.first();
if (!firstSelection)
return;
diagram.startTransaction('aligning Center Y');
const centerY = firstSelection.actualBounds.y + firstSelection.actualBounds.height / 2;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over go.Link
current.moveTo(current.actualBounds.x, centerY - current.actualBounds.height / 2);
});
diagram.commitTransaction('aligning Center Y');
}
/**
* Aligns selected parts top-to-bottom in order of the order selected.
* Distance between parts can be specified. Default distance is 0.
*/
alignColumn(distance) {
if (distance === undefined)
distance = 0; // for aligning edge to edge
const diagram = this.diagram;
const firstSelection = diagram.selection.first();
if (!firstSelection)
return;
diagram.startTransaction('aligning Column');
const centerX = firstSelection.actualBounds.centerX;
let y = firstSelection.actualBounds.top;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over links
current.moveTo(centerX - current.actualBounds.width / 2, y);
y += current.actualBounds.height + distance;
});
diagram.commitTransaction('aligning Column');
}
/**
* Aligns selected parts left-to-right in order of the order selected.
* Distance between parts can be specified. Default distance is 0.
*/
alignRow(distance) {
if (distance === undefined)
distance = 0; // for aligning edge to edge
const diagram = this.diagram;
const firstSelection = diagram.selection.first();
if (!firstSelection)
return;
diagram.startTransaction('aligning Row');
const centerY = firstSelection.actualBounds.centerY;
let x = firstSelection.actualBounds.left;
diagram.selection.each((current) => {
if (current instanceof go.Link)
return; // skips over links
current.moveTo(x, centerY - current.actualBounds.height / 2);
x += current.actualBounds.width + distance;
});
diagram.commitTransaction('aligning Row');
}
/**
* Position each selected non-Link horizontally so that each distance between them is the same,
* given the total width of the area occupied by them.
* Their Y positions are not modified.
* It tries to maintain the same ordering of selected Parts by their X position.
*
* Note that if there is not enough room, the spacing might be negative -- the Parts might overlap.
*/
spaceEvenlyHorizontally() {
const diagram = this.diagram;
const nonlinks = new go.List();
diagram.selection.each((part) => {
if (part instanceof go.Link)
return; // skips over links
nonlinks.add(part); // maybe check for non-movable Parts??
});
if (nonlinks.count <= 1)
return;
const b = diagram.computePartsBounds(nonlinks);
if (!b.isReal())
return;
nonlinks.sort((n, m) => n.actualBounds.x - m.actualBounds.x);
let w = 0;
nonlinks.each((part) => (w += part.actualBounds.width));
const sp = (b.width - w) / (nonlinks.count - 1); // calculate available space between nodes; might be negative
diagram.startTransaction('space evenly horizontally');
let x = b.x;
nonlinks.each((part) => {
part.moveTo(x, part.actualBounds.y);
x += part.actualBounds.width + sp;
});
diagram.commitTransaction('space evenly horizontally');
}
/**
* Position each selected non-Link vertically so that each distance between them is the same,
* given the total height of the area occupied by them.
* Their X positions are not modified.
* It tries to maintain the same ordering of selected Parts by their Y position.
*
* Note that if there is not enough room, the spacing might be negative -- the Parts might overlap.
*/
spaceEvenlyVertically() {
const diagram = this.diagram;
const nonlinks = new go.List();
diagram.selection.each((part) => {
if (part instanceof go.Link)
return; // skips over links
nonlinks.add(part); // maybe check for non-movable Parts??
});
if (nonlinks.count <= 1)
return;
const b = diagram.computePartsBounds(nonlinks);
if (!b.isReal())
return;
nonlinks.sort((n, m) => n.actualBounds.y - m.actualBounds.y);
let h = 0;
nonlinks.each((part) => (h += part.actualBounds.height));
const sp = (b.height - h) / (nonlinks.count - 1); // calculate available space between nodes; might be negative
diagram.startTransaction('space evenly vertically');
let y = b.y;
nonlinks.each((part) => {
part.moveTo(part.actualBounds.x, y);
y += part.actualBounds.height + sp;
});
diagram.commitTransaction('space evenly vertically');
}
/**
* This controls whether or not the user can invoke the {@link rotate} command.
* @returns This returns true:
* if the diagram is not {@link go.Diagram.isReadOnly},
* if the model is not {@link go.Model.isReadOnly}, and
* if there is at least one selected {@link go.Part}.
*/
canRotate() {
const diagram = this.diagram;
if (diagram.isReadOnly || diagram.isModelReadOnly)
return false;
if (diagram.selection.count < 1)
return false;
return true;
}
/**
* Change the angle of the parts connected with the given part. This is in the command handler
* so it can be easily accessed for the purpose of creating commands that change the rotation of a part.
* @param angle - the positive (clockwise) or negative (counter-clockwise) change in the rotation angle of each Part, in degrees.
*/
rotate(angle) {
if (angle === undefined)
angle = 90;
const diagram = this.diagram;
diagram.startTransaction('rotate ' + angle.toString());
diagram.selection.each((current) => {
if (current instanceof go.Link || current instanceof go.Group)
return; // skips over Links and Groups
current.angle += angle;
});
diagram.commitTransaction('rotate ' + angle.toString());
}
/**
* Change the z-ordering of selected parts to pull them forward, in front of all other parts
* in their respective layers.
* All unselected parts in each layer with a selected Part with a non-numeric {@link go.Part.zOrder} will get a zOrder of zero.
*/
pullToFront() {
const diagram = this.diagram;
diagram.startTransaction('pullToFront');
// find the affected Layers
const layers = new Map();
diagram.selection.each((part) => {
if (part.layer !== null)
layers.set(part.layer, 0);
});
// find the maximum zOrder in each Layer
for (const layer of layers.keys()) {
let max = 0;
layer.parts.each((part) => {
if (part.isSelected)
return;
const z = part.zOrder;
if (isNaN(z)) {
part.zOrder = 0;
}
else {
max = Math.max(max, z);
}
});
layers.set(layer, max);
}
// assign each selected Part.zOrder to the computed value for each Layer
diagram.selection.each((part) => {
const z = layers.get(part.layer) || 0;
DrawCommandHandler._assignZOrder(part, z + 1);
});
diagram.commitTransaction('pullToFront');
}
/**
* Change the z-ordering of selected parts to push them backward, behind of all other parts
* in their respective layers.
* All unselected parts in each layer with a selected Part with a non-numeric {@link go.Part.zOrder} will get a zOrder of zero.
*/
pushToBack() {
const diagram = this.diagram;
diagram.startTransaction('pushToBack');
// find the affected Layers
const layers = new Map();
diagram.selection.each((part) => {
if (part.layer !== null)
layers.set(part.layer, 0);
});
// find the minimum zOrder in each Layer
for (const layer of layers.keys()) {
let min = 0;
layer.parts.each((part) => {
if (part.isSelected)
return;
const z = part.zOrder;
if (isNaN(z)) {
part.zOrder = 0;
}
else {
min = Math.min(min, z);
}
});
layers.set(layer, min);
}
// assign each selected Part.zOrder to the computed value for each Layer
diagram.selection.each((part) => {
const z = layers.get(part.layer) || 0;
DrawCommandHandler._assignZOrder(part,
// make sure a group's nested nodes are also behind everything else
z - 1 - DrawCommandHandler._findGroupDepth(part));
});
diagram.commitTransaction('pushToBack');
}
static _assignZOrder(part, z, root) {
if (root === undefined)
root = part;
if (part.layer === root.layer)
part.zOrder = z;
if (part instanceof go.Group) {
part.memberParts.each((m) => {
DrawCommandHandler._assignZOrder(m, z + 1, root);
});
}
}
static _findGroupDepth(part) {
if (part instanceof go.Group) {
let d = 0;
part.memberParts.each((m) => {
d = Math.max(d, DrawCommandHandler._findGroupDepth(m));
});
return d + 1;
}
else {
return 0;
}
}
/**
* This implements custom behaviors for arrow key keyboard events.
* Set {@link arrowKeyBehavior} to "select", "move" (the default), "scroll" (the standard behavior), or "none"
* to affect the behavior when the user types an arrow key.
* Set that property to "default" in order to make use of the additional commands in this class
* without affecting the arrow key behaviors.
*
* Note that this functionality is different from the focus navigation behavior of the {@link CommandHandler}
* that was added in version 3.1 and enabled by the {@link CommandHandler.isFocusEnabled} property.
* In this DrawCommandHandler the arrow keys for the "move", "select" or "tree" behaviors
* depend on and modify the {@link Diagram.selection}. The built-in focus navigation is completely independent
* of the selection mechanism.
*/
doKeyDown() {
const diagram = this.diagram;
const e = diagram.lastInput;
// determines the function of the arrow keys
if (e.code === 'ArrowUp' ||
e.code === 'ArrowDown' ||
e.code === 'ArrowLeft' ||
e.code === 'ArrowRight') {
const behavior = this.arrowKeyBehavior;
if (behavior === 'none') {
// no-op
return;
}
else if (behavior === 'select') {
this._arrowKeySelect();
return;
}
else if (behavior === 'move') {
this._arrowKeyMove();
return;
}
else if (behavior === 'tree') {
this._arrowKeyTree();
return;
}
// otherwise drop through to get the default scrolling behavior
}
// otherwise still does all standard commands
super.doKeyDown();
}
/**
* Collects in an Array all of the non-Link Parts currently in the Diagram.
*/
_getAllParts() {
const allParts = new Array();
this.diagram.nodes.each((node) => {
allParts.push(node);
});
this.diagram.parts.each((part) => {
allParts.push(part);
});
// note that this ignores Links
return allParts;
}
/**
* To be called when arrow keys should move the Diagram.selection.
*/
_arrowKeyMove() {
const diagram = this.diagram;
const e = diagram.lastInput;
// moves all selected parts in the specified direction
let vdistance = 0;
let hdistance = 0;
// if control is being held down, move pixel by pixel. Else, moves by grid cell size
if (e.control || e.meta) {
vdistance = 1;
hdistance = 1;
}
else if (diagram.grid !== null) {
const cellsize = diagram.grid.gridCellSize;
hdistance = cellsize.width;
vdistance = cellsize.height;
}
diagram.startTransaction('arrowKeyMove');
diagram.selection.each((part) => {
if (e.code === 'ArrowUp') {
part.moveTo(part.actualBounds.x, part.actualBounds.y - vdistance);
}
else if (e.code === 'ArrowDown') {
part.moveTo(part.actualBounds.x, part.actualBounds.y + vdistance);
}
else if (e.code === 'ArrowLeft') {
part.moveTo(part.actualBounds.x - hdistance, part.actualBounds.y);
}
else if (e.code === 'ArrowRight') {
part.moveTo(part.actualBounds.x + hdistance, part.actualBounds.y);
}
});
diagram.commitTransaction('arrowKeyMove');
}
/**
* To be called when arrow keys should change selection.
*/
_arrowKeySelect() {
const diagram = this.diagram;
const e = diagram.lastInput;
// with a part selected, arrow keys change the selection
// arrow keys + shift selects the additional part in the specified direction
// arrow keys + control toggles the selection of the additional part
let nextPart = null;
if (e.code === 'ArrowUp') {
nextPart = this._findNearestPartTowards(270);
}
else if (e.code === 'ArrowDown') {
nextPart = this._findNearestPartTowards(90);
}
else if (e.code === 'ArrowLeft') {
nextPart = this._findNearestPartTowards(180);
}
else if (e.code === 'ArrowRight') {
nextPart = this._findNearestPartTowards(0);
}
if (nextPart !== null) {
if (e.shift) {
nextPart.isSelected = true;
}
else if (e.control || e.meta) {
nextPart.isSelected = !nextPart.isSelected;
}
else {
diagram.select(nextPart);
}
}
}
/**
* Finds the nearest selectable Part in the specified direction, based on their center points.
* if it doesn't find anything, it just returns the current Part.
* @param dir - the direction, in degrees
* @returns the closest Part found in the given direction
*/
_findNearestPartTowards(dir) {
const originalPart = this.diagram.selection.first();
if (originalPart === null)
return null;
const originalPoint = originalPart.actualBounds.center;
const allParts = this._getAllParts();
let closestDistance = Infinity;
let closest = originalPart; // if no parts meet the criteria, the same part remains selected
for (let i = 0; i < allParts.length; i++) {
const nextPart = allParts[i];
if (nextPart === originalPart)
continue; // skips over currently selected part
if (!nextPart.canSelect())
continue;
const nextPoint = nextPart.actualBounds.center;
const angle = originalPoint.directionPoint(nextPoint);
const anglediff = this._angleCloseness(angle, dir);
if (anglediff <= 45) {
// if this part's center is within the desired direction's sector,
let distance = originalPoint.distanceSquaredPoint(nextPoint);
distance *= 1 + Math.sin((anglediff * Math.PI) / 180); // the more different from the intended angle, the further it is
if (distance < closestDistance) {
// and if it's closer than any other part,
closestDistance = distance; // remember it as a better choice
closest = nextPart;
}
}
}
return closest;
}
_angleCloseness(a, dir) {
return Math.min(Math.abs(dir - a), Math.min(Math.abs(dir + 360 - a), Math.abs(dir - 360 - a)));
}
/**
* To be called when arrow keys should change the selected node in a tree and expand or collapse subtrees.
*/
_arrowKeyTree() {
const diagram = this.diagram;
let selected = diagram.selection.first();
if (!(selected instanceof go.Node))
return;
const e = diagram.lastInput;
if (e.code === 'ArrowRight') {
if (selected.isTreeLeaf) {
// no-op
}
else if (!selected.isTreeExpanded) {
if (diagram.commandHandler.canExpandTree(selected)) {
diagram.commandHandler.expandTree(selected); // expands the tree
}
}
else {
// already expanded -- select the first child node
const first = this._sortTreeChildrenByY(selected).first();
if (first !== null)
diagram.select(first);
}
}
else if (e.code === 'ArrowLeft') {
if (!selected.isTreeLeaf && selected.isTreeExpanded) {
if (diagram.commandHandler.canCollapseTree(selected)) {
diagram.commandHandler.collapseTree(selected); // collapses the tree
}
}
else {
// either a leaf or is already collapsed -- select the parent node
const parent = selected.findTreeParentNode();
if (parent !== null)
diagram.select(parent);
}
}
else if (e.code === 'ArrowUp') {
const parent = selected.findTreeParentNode();
if (parent !== null) {
const list = this._sortTreeChildrenByY(parent);
const idx = list.indexOf(selected);
if (idx > 0) {
// if there is a previous sibling
let prev = list.elt(idx - 1);
// keep looking at the last child until it's a leaf or collapsed
while (prev !== null && prev.isTreeExpanded && !prev.isTreeLeaf) {
const children = this._sortTreeChildrenByY(prev);
prev = children.last();
}
if (prev !== null)
diagram.select(prev);
}
else {
// no previous sibling -- select parent
diagram.select(parent);
}
}
}
else if (e.code === 'ArrowDown') {
// if at an expanded parent, select the first child
if (selected.isTreeExpanded && !selected.isTreeLeaf) {
const first = this._sortTreeChildrenByY(selected).first();
if (first !== null)
diagram.select(first);
}
else {
while (selected instanceof go.Node) {
const parent = selected.findTreeParentNode();
if (parent === null)
break;
const list = this._sortTreeChildrenByY(parent);
const idx = list.indexOf(selected);
if (idx < list.length - 1) {
// select next lower node
diagram.select(list.elt(idx + 1));
break;
}
else {
// already at bottom of list of children
selected = parent;
}
}
}
}
// make sure the selection is now in the viewport, but not necessarily centered
const sel = diagram.selection.first();
if (sel !== null)
diagram.scrollToRect(sel.actualBounds);
}
_sortTreeChildrenByY(node) {
const list = new go.List().addAll(node.findTreeChildrenNodes());
list.sort((a, b) => {
const aloc = a.location;
const bloc = b.location;
if (aloc.y < bloc.y)
return -1;
if (aloc.y > bloc.y)
return 1;
if (aloc.x < bloc.x)
return -1;
if (aloc.x > bloc.x)
return 1;
return 0;
});
return list;
}
/**
* Reset the last offset for pasting.
* @param coll - a collection of {@link go.Part}s.
*/
copyToClipboard(coll) {
super.copyToClipboard(coll);
this._lastPasteOffset.set(this.pasteOffset);
}
/**
* Paste from the clipboard with an offset incremented on each paste, and reset when copied.
* @returns a collection of newly pasted {@link go.Part}s
*/
pasteFromClipboard() {
const coll = super.pasteFromClipboard();
this.diagram.moveParts(coll, this._lastPasteOffset, false);
this._lastPasteOffset.add(this.pasteOffset);
return coll;
}
// Saving and loading Models as files on local file system
/** @hidden @internal */
saveFile(name, mimetype, contents) {
let url = null;
let a = null;
try {
const blob = new Blob([contents], { type: mimetype });
url = window.URL.createObjectURL(blob);
a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = name;
document.body.appendChild(a);
requestAnimationFrame(() => {
try {
if (a !== null)
a.click();
}
finally {
if (url !== null)
window.URL.revokeObjectURL(url);
if (a !== null)
document.body.removeChild(a);
}
});
}
catch (ex) {
if (url !== null)
window.URL.revokeObjectURL(url);
if (a !== null)
document.body.removeChild(a);
}
}
/**
* This command downloads a text file that holds this diagram's model as JSON-formatted text.
*
* This calls {@link Model.toJson}.
* @param options an optional file name (defaults to {@link Model.name}.{@link localFileType}) and
* an optional MIME type (defaults to "application/text")
* @since 3.1
*/
saveLocalFile(options) {
const diagram = this.diagram;
let name = options === null || options === void 0 ? void 0 : options.name;
if (!name)
name = this.defaultFilename();
let type = options === null || options === void 0 ? void 0 : options.mimetype;
if (!type)
type = 'application/text';
const text = diagram.model.toJson();
diagram.isModified = false;
this.saveFile(name, type, text);
}
/**
* This predicate controls whether or not the user can invoke the {@link saveLocalFile} command.
*
* @returns true, by default
* @since 3.1
*/
canSaveLocalFile() {
const diagram = this.diagram;
return diagram !== null;
}
/** @hidden @internal */
defaultFilename() {
const filetypeending = '.' + this.localFileType;
let name = this.diagram.model.name;
if (!name) {
name = 'diagram';
}
else if (name.endsWith(filetypeending)) {
name = name.substring(0, name.length - filetypeending.length);
}
name += filetypeending;
return name;
}
/**
* Gets or sets the default file type for locally saved files.
* The default value is "gojs".
* Setting this property does not raise any events.
* @since 3.1
*/
get localFileType() { return this._localFileType; }
set localFileType(t) { this._localFileType = t || ''; }
/**
* This method loads a text file that the user chooses or drops that holds this diagram's model as JSON-formatted text,
* normally saved via {@link saveLocalFile}.
*
* This calls {@link Model.fromJson}.
* This is called by the "change" event of the {@link localFileInput} element (if present) or
* the "drop" event of the {@link localFileDropElement} (if present).
*
* The file type of the File.name must match the {@link localFileType}, or it must not have a file type.
* @param file a File instance from which to read the JSON-formatted text
* @param loader an optional function that sets {@link Diagram.model}, perhaps modifying the model first,
* and perhaps doing other updates after assigning the given Model to the given Diagram.
* @since 3.1
*/
loadLocalFile(file, loader) {
const diagram = this.diagram;
let type = '';
let name = file.name;
const lastdot = name.lastIndexOf('.');
if (lastdot > 0) {
type = name.substring(lastdot + 1).toLowerCase();
name = name.substring(0, lastdot);
}
if (type === '' || type === this.localFileType) {
diagram.currentCursor = 'progress';
requestAnimationFrame(() => {
const reader = new FileReader();
reader.onload = e => {
var _a;
if (typeof ((_a = e.target) === null || _a === void 0 ? void 0 : _a.result) === 'string') {
const newmodel = go.Model.fromJson(e.target.result);
if (loader) {
loader(diagram, newmodel, name);
}
else {
if (!newmodel.name)
newmodel.name = name;
diagram.model = newmodel;
}
}
};
reader.readAsText(file);
});
}
}
/**
* Gets or sets an HTMLElement so that the user can load a file saved by {@link saveLocalFile}
* by drag-and-dropping it on this element.
*
* By default the value is null -- there is no such element.
* Setting this property does not raise any events or modify the DOM, but does add or remove listeners,
* including a "drop" listener that actually calls {@link loadLocalFile} and {@link Diagram.focus}.
*
* If you want to support drag-and-drop loading of files,
* you will need to add the element to your page and set this property.
* @see {@link localFileInput}
* @since 3.1
*/
get localFileDropElement() { return this._localFileDropElement; }
set localFileDropElement(val) {
const old = this._localFileDropElement;
if (old != val) {
if (old) {
old.removeEventListener('dragenter', this._preventPropagation);
old.removeEventListener('dragover', this._preventPropagation);
old.removeEventListener('drop', this._handleDrop);
}
this._localFileDropElement = val;
if (val) {
val.addEventListener('dragenter', this._preventPropagation);
val.addEventListener('dragover', this._preventPropagation);
val.addEventListener('drop', this._handleDrop);
}
}
}
/**
* Gets or sets an HTMLInputElement so that the user can load a file saved by {@link saveLocalFile}
* by using the browser's file picker user interface.
*
* By default the value is null -- there is no such input element.
* Setting this property does not raise any events or modify the DOM, but does add or remove a "change" listener
* that actually calls {@link loadLocalFile} and {@link Diagram.focus}.
*
* If you want to support the user's picking of a file to load,
* you will need to add an <input type="file"> element to your page and set this property.
* It is moderately common to have this input element be hidden and invoke the input file picker by programmatically
* calling <code>click()</code> on the element.
* @see {@link localFileDropElement}
* @since 3.1
*/
get localFileInput() { return this._localFileInput; }
set localFileInput(val) {
const old = this._localFileInput;
if (old !== val) {
if (old) {
old.removeEventListener('change', this._handleFileInputChange);
}
this._localFileInput = val;
if (val) {
//val.value = '';
val.addEventListener('change', this._handleFileInputChange, false);
}
}
}
/**
* Gets or sets a function that is used to set {@link Diagram.model}.
* It can do more things before and/or after the actual setting of {@link Diagram.model}.
*
* The default value is null.
* Setting this property does not raise any events.
*
* If non-null, the function is called by {@link loadLocalFile}.
*/
get loader() { return this._loader; }
set loader(func) { this._loader = func; }
}