svgedit
Version:
Powerful SVG-Editor for your browser
1,410 lines (1,296 loc) • 95.8 kB
JavaScript
/**
* Numerous tools for working with the editor's "canvas".
* @module svgcanvas
*
* @license MIT
*
* @copyright 2010 Alexis Deveria, 2010 Pavol Rusnak, 2010 Jeff Schiller
*
*/
import { Canvg as canvg } from 'canvg';
import 'pathseg'; // SVGPathSeg Polyfill (see https://github.com/progers/pathseg)
import * as pathModule from './path.js';
import * as hstry from './history.js';
import * as draw from './draw.js';
import {
init as pasteInit, pasteElementsMethod
} from './paste-elem.js';
// eslint-disable-next-line no-duplicate-imports
import {
identifyLayers, createLayer, cloneLayer, deleteCurrentLayer,
setCurrentLayer, renameCurrentLayer, setCurrentLayerPosition,
setLayerVisibility, moveSelectedToLayer, mergeLayer, mergeAllLayers,
leaveContext, setContext
} from './draw.js';
import { svgRootElement } from './svgroot.js';
import {
init as undoInit, getUndoManager, changeSelectedAttributeNoUndoMethod,
changeSelectedAttributeMethod, ffClone
} from './undo.js';
import {
init as selectionInit, clearSelectionMethod, addToSelectionMethod, getMouseTargetMethod,
getIntersectionListMethod, runExtensionsMethod, groupSvgElem, prepareSvg,
recalculateAllSelectedDimensions, setRotationAngle
} from './selection.js';
import {
init as textActionsInit, textActionsMethod
} from './text-actions.js';
import {
init as eventInit, mouseMoveEvent, mouseUpEvent, mouseOutEvent,
dblClickEvent, mouseDownEvent, DOMMouseScrollEvent
} from './event.js';
import { init as jsonInit, getJsonFromSvgElements, addSVGElementsFromJson } from './json.js';
import {
init as elemInit, getResolutionMethod, getTitleMethod, setGroupTitleMethod,
setDocumentTitleMethod, setResolutionMethod, getEditorNSMethod, setBBoxZoomMethod,
setZoomMethod, setColorMethod, setGradientMethod, findDuplicateGradient, setPaintMethod,
setStrokeWidthMethod, setStrokeAttrMethod, getBoldMethod, setBoldMethod, getItalicMethod,
setItalicMethod, setTextAnchorMethod, getFontFamilyMethod, setFontFamilyMethod, setFontColorMethod, getFontColorMethod,
getFontSizeMethod, setFontSizeMethod, getTextMethod, setTextContentMethod,
setImageURLMethod, setLinkURLMethod, setRectRadiusMethod, makeHyperlinkMethod,
removeHyperlinkMethod, setSegTypeMethod, setBackgroundMethod
} from './elem-get-set.js';
import {
init as selectedElemInit, moveToTopSelectedElem, moveToBottomSelectedElem,
moveUpDownSelected, moveSelectedElements, cloneSelectedElements, alignSelectedElements,
deleteSelectedElements, copySelectedElements, groupSelectedElements, pushGroupProperty,
ungroupSelectedElement, cycleElement, updateCanvas
} from './selected-elem.js';
import {
init as blurInit, setBlurNoUndo, setBlurOffsets, setBlur
} from './blur-event.js';
import { sanitizeSvg } from './sanitize.js';
import { getReverseNS, NS } from './namespaces.js';
import {
text2xml, assignAttributes, cleanupElement, getElem, getUrlFromAttr,
findDefs, getHref, setHref, getRefElem, getRotationAngle, getPathBBox,
preventClickDefault, walkTree, getBBoxOfElementAsPath, convertToPath, encode64, decode64,
getVisibleElements, dropXMLInternalSubset, init as utilsInit,
getBBox as utilsGetBBox, getStrokedBBoxDefaultVisible, isNullish, blankPageObjectURL,
$id, $qa, $qq, getFeGaussianBlur, stringToHTML, insertChildAtIndex
} from './utilities.js';
import {
transformPoint, matrixMultiply, hasMatrixTransform, transformListToTransform,
isIdentity, transformBox
} from './math.js';
import {
convertToNum, getTypeMap, init as unitsInit
} from '../common/units.js';
import {
svgCanvasToString, svgToString, setSvgString, exportPDF, setUseDataMethod,
init as svgInit, importSvgString, embedImage, rasterExport,
uniquifyElemsMethod, removeUnusedDefElemsMethod, convertGradientsMethod
} from './svg-exec.js';
import {
isChrome, isWebkit
} from '../common/browser.js'; // , supportsEditableText
import {
remapElement,
init as coordsInit
} from './coords.js';
import {
recalculateDimensions,
init as recalculateInit
} from './recalculate.js';
import {
getSelectorManager,
Selector,
init as selectInit
} from './select.js';
import {
clearSvgContentElementInit,
init as clearInit
} from './clear.js';
import {
getClosest, getParents, mergeDeep
} from '../editor/components/jgraduate/Util.js';
const {
MoveElementCommand, InsertElementCommand, RemoveElementCommand,
ChangeElementCommand, BatchCommand
} = hstry;
const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';
const refAttrs = [ 'clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
if (!window.console) {
window.console = {};
window.console.log = function (_str) { /* empty fn */ };
window.console.dir = function (_str) { /* empty fn */ };
}
if (window.opera) {
window.console.log = function (str) { window.opera.postError(str); };
window.console.dir = function (_str) { /* empty fn */ };
}
// Reenable after fixing eslint-plugin-jsdoc to handle
/**
* The main SvgCanvas class that manages all SVG-related functions.
* @memberof module:svgcanvas
*
* @borrows module:coords.remapElement as #remapElement
* @borrows module:recalculate.recalculateDimensions as #recalculateDimensions
*
* @borrows module:utilities.cleanupElement as #cleanupElement
* @borrows module:utilities.getStrokedBBoxDefaultVisible as #getStrokedBBox
* @borrows module:utilities.getVisibleElements as #getVisibleElements
* @borrows module:utilities.findDefs as #findDefs
* @borrows module:utilities.getUrlFromAttr as #getUrlFromAttr
* @borrows module:utilities.getHref as #getHref
* @borrows module:utilities.setHref as #setHref
* @borrows module:utilities.getRotationAngle as #getRotationAngle
* @borrows module:utilities.getBBox as #getBBox
* @borrows module:utilities.getElem as #getElem
* @borrows module:utilities.getRefElem as #getRefElem
* @borrows module:utilities.assignAttributes as #assignAttributes
*
* @borrows module:math.matrixMultiply as #matrixMultiply
* @borrows module:math.hasMatrixTransform as #hasMatrixTransform
* @borrows module:math.transformListToTransform as #transformListToTransform
* @borrows module:units.convertToNum as #convertToNum
* @borrows module:sanitize.sanitizeSvg as #sanitizeSvg
* @borrows module:path.pathActions.linkControlPoints as #linkControlPoints
*/
class SvgCanvas {
/**
* @param {HTMLElement} container - The container HTML element that should hold the SVG root element
* @param {module:SVGeditor.configObj.curConfig} config - An object that contains configuration data
*/
constructor(container, config) {
// Alias Namespace constants
// Default configuration options
let curConfig = {
show_outside_canvas: true,
selectNew: true,
dimensions: [ 640, 480 ]
};
// Update config with new one if given
this.mergeDeep = mergeDeep;
if (config) {
curConfig = this.mergeDeep(curConfig, config);
}
// Array with width/height of canvas
const { dimensions } = curConfig;
const canvas = this;
this.$id = $id;
this.$qq = $qq;
this.$qa = $qa;
this.encode64 = encode64;
this.decode64 = decode64;
this.stringToHTML = stringToHTML;
this.insertChildAtIndex = insertChildAtIndex;
this.getClosest = getClosest;
this.getParents = getParents;
/** A storage solution aimed at replacing jQuerys data function.
* Implementation Note: Elements are stored in a (WeakMap)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap].
* This makes sure the data is garbage collected when the node is removed.
*/
this.dataStorage = {
_storage: new WeakMap(),
put: function (element, key, obj) {
if (!this._storage.has(element)) {
this._storage.set(element, new Map());
}
this._storage.get(element).set(key, obj);
},
get: function (element, key) {
return this._storage.get(element)?.get(key);
},
has: function (element, key) {
return this._storage.has(element) && this._storage.get(element).has(key);
},
remove: function (element, key) {
const ret = this._storage.get(element).delete(key);
if (!this._storage.get(element).size === 0) {
this._storage.delete(element);
}
return ret;
}
};
const getDataStorage = this.getDataStorage = function () { return canvas.dataStorage; };
this.isLayer = draw.Layer.isLayer;
// "document" element associated with the container (same as window.document using default svg-editor.js)
// NOTE: This is not actually a SVG document, but an HTML document.
const svgdoc = window.document;
// This is a container for the document being edited, not the document itself.
/**
* @name module:svgcanvas~svgroot
* @type {SVGSVGElement}
*/
const svgroot = svgRootElement(svgdoc, dimensions);
const getSVGRoot = () => svgroot;
container.append(svgroot);
/**
* The actual element that represents the final output SVG element.
* @name module:svgcanvas~svgcontent
* @type {SVGSVGElement}
*/
let svgcontent = svgdoc.createElementNS(NS.SVG, 'svg');
/**
* This should really be an intersection implementing all rather than a union.
* @type {module:draw.DrawCanvasInit#getSVGContent|module:utilities.EditorContext#getSVGContent}
*/
const getSVGContent = () => { return svgcontent; };
clearInit(
/**
* @implements {module:utilities.EditorContext}
*/
{
getSVGContent,
getDOMDocument() { return svgdoc; },
getDOMContainer() { return container; },
getSVGRoot,
getCurConfig() { return curConfig; }
}
);
/**
* This function resets the svgcontent element while keeping it in the DOM.
* @function module:svgcanvas.SvgCanvas#clearSvgContentElement
* @returns {void}
*/
const clearSvgContentElement = canvas.clearSvgContentElement = clearSvgContentElementInit;
clearSvgContentElement();
// Prefix string for element IDs
let idprefix = 'svg_';
/**
* Changes the ID prefix to the given value.
* @function module:svgcanvas.SvgCanvas#setIdPrefix
* @param {string} p - String with the new prefix
* @returns {void}
*/
canvas.setIdPrefix = function (p) {
idprefix = p;
};
/**
* Current `draw.Drawing` object.
* @type {module:draw.Drawing}
* @name module:svgcanvas.SvgCanvas#current_drawing_
*/
canvas.current_drawing_ = new draw.Drawing(svgcontent, idprefix);
/**
* Returns the current Drawing.
* @name module:svgcanvas.SvgCanvas#getCurrentDrawing
* @type {module:draw.DrawCanvasInit#getCurrentDrawing}
*/
const getCurrentDrawing = canvas.getCurrentDrawing = function () {
return canvas.current_drawing_;
};
/**
* Float displaying the current zoom level (1 = 100%, .5 = 50%, etc.).
* @type {Float}
*/
let currentZoom = 1;
// pointer to current group (for in-group editing)
let currentGroup = null;
// Object containing data for the currently selected styles
const allProperties = {
shape: {
fill: (curConfig.initFill.color === 'none' ? '' : '#') + curConfig.initFill.color,
fill_paint: null,
fill_opacity: curConfig.initFill.opacity,
stroke: '#' + curConfig.initStroke.color,
stroke_paint: null,
stroke_opacity: curConfig.initStroke.opacity,
stroke_width: curConfig.initStroke.width,
stroke_dasharray: 'none',
stroke_linejoin: 'miter',
stroke_linecap: 'butt',
opacity: curConfig.initOpacity
}
};
allProperties.text = this.mergeDeep({}, allProperties.shape);
allProperties.text = this.mergeDeep(allProperties.text, {
fill: '#000000',
stroke_width: curConfig.text && curConfig.text.stroke_width,
font_size: curConfig.text && curConfig.text.font_size,
font_family: curConfig.text && curConfig.text.font_family
});
// Current shape style properties
const curShape = allProperties.shape;
// Array with all the currently selected elements
// default size of 1 until it needs to grow bigger
let selectedElements = [];
jsonInit(
/**
* @implements {module:json.jsonContext}
*/
{
getDOMDocument() { return svgdoc; },
getDrawing() { return getCurrentDrawing(); },
getCurShape() { return curShape; },
getCurrentGroup() { return currentGroup; }
}
);
/**
* @typedef {PlainObject} module:svgcanvas.SVGAsJSON
* @property {string} element
* @property {PlainObject<string, string>} attr
* @property {module:svgcanvas.SVGAsJSON[]} children
*/
/**
* @function module:svgcanvas.SvgCanvas#getContentElem
* @param {Text|Element} data
* @returns {module:svgcanvas.SVGAsJSON}
*/
const getJsonFromSvgElement = this.getJsonFromSvgElement = getJsonFromSvgElements;
/**
* This should really be an intersection implementing all rather than a union.
* @name module:svgcanvas.SvgCanvas#addSVGElementFromJson
* @type {module:utilities.EditorContext#addSVGElementFromJson|module:path.EditorContext#addSVGElementFromJson}
*/
const addSVGElementFromJson = this.addSVGElementFromJson = addSVGElementsFromJson;
canvas.matrixMultiply = matrixMultiply;
canvas.hasMatrixTransform = hasMatrixTransform;
canvas.transformListToTransform = transformListToTransform;
/**
* @type {module:utilities.EditorContext#getBaseUnit}
*/
const getBaseUnit = () => { return curConfig.baseUnit; };
/**
* Initialize from units.js.
* Send in an object implementing the ElementContainer interface (see units.js).
*/
unitsInit(
/**
* @implements {module:units.ElementContainer}
*/
{
getBaseUnit,
getElement: getElem,
getHeight() { return svgcontent.getAttribute('height') / currentZoom; },
getWidth() { return svgcontent.getAttribute('width') / currentZoom; },
getRoundDigits() { return saveOptions.round_digits; }
}
);
canvas.convertToNum = convertToNum;
/**
* Should really be an intersection with all needing to apply rather than a union.
* @name module:svgcanvas.SvgCanvas#getSelectedElements
* @type {module:utilities.EditorContext#getSelectedElements|module:draw.DrawCanvasInit#getSelectedElements|module:path.EditorContext#getSelectedElements}
*/
const getSelectedElements = this.getSelectedElems = function () {
return selectedElements;
};
this.setSelectedElements = function (key, value) {
selectedElements[key] = value;
};
this.setEmptySelectedElements = function () {
selectedElements = [];
};
const { pathActions } = pathModule;
/**
* This should actually be an intersection as all interfaces should be met.
* @type {module:utilities.EditorContext#getSVGRoot|module:recalculate.EditorContext#getSVGRoot|module:coords.EditorContext#getSVGRoot|module:path.EditorContext#getSVGRoot}
*/
utilsInit(
/**
* @implements {module:utilities.EditorContext}
*/
{
pathActions, // Ok since not modifying
getSVGContent,
addSVGElementFromJson,
getSelectedElements,
getDOMDocument() { return svgdoc; },
getDOMContainer() { return container; },
getSVGRoot,
// TODO: replace this mostly with a way to get the current drawing.
getBaseUnit,
getSnappingStep() { return curConfig.snappingStep; },
getDataStorage
}
);
canvas.findDefs = findDefs;
canvas.getUrlFromAttr = getUrlFromAttr;
canvas.getHref = getHref;
canvas.setHref = setHref;
/* const getBBox = */ canvas.getBBox = utilsGetBBox;
canvas.getRotationAngle = getRotationAngle;
canvas.getElem = getElem;
canvas.getRefElem = getRefElem;
canvas.assignAttributes = assignAttributes;
this.cleanupElement = cleanupElement;
/**
* This should actually be an intersection not a union as all should apply.
* @type {module:coords.EditorContext#getGridSnapping|module:path.EditorContext#getGridSnapping}
*/
const getGridSnapping = () => { return curConfig.gridSnapping; };
coordsInit(
/**
* @implements {module:coords.EditorContext}
*/
{
getDrawing() { return getCurrentDrawing(); },
getDataStorage,
getSVGRoot,
getGridSnapping
}
);
this.remapElement = remapElement;
recalculateInit(
/**
* @implements {module:recalculate.EditorContext}
*/
{
getSVGRoot,
getStartTransform() { return startTransform; },
setStartTransform(transform) { startTransform = transform; },
getDataStorage
}
);
this.recalculateDimensions = recalculateDimensions;
// import from sanitize.js
const nsMap = getReverseNS();
canvas.sanitizeSvg = sanitizeSvg;
/**
* This should really be an intersection applying to all types rather than a union.
* @name module:svgcanvas.SvgCanvas#getZoom
* @type {module:path.EditorContext#getCurrentZoom|module:select.SVGFactory#getCurrentZoom}
*/
const getCurrentZoom = this.getZoom = function () { return currentZoom; };
/**
* This method rounds the incoming value to the nearest value based on the `currentZoom`
* @name module:svgcanvas.SvgCanvas#round
* @type {module:path.EditorContext#round}
*/
const round = this.round = function (val) {
return Number.parseInt(val * currentZoom) / currentZoom;
};
selectInit(
curConfig,
/**
* Export to select.js.
* @implements {module:select.SVGFactory}
*/
{
createSVGElement(jsonMap) { return canvas.addSVGElementFromJson(jsonMap); },
svgRoot() { return svgroot; },
svgContent() { return svgcontent; },
getDataStorage,
getCurrentZoom
}
);
/**
* This object manages selectors for us.
* @name module:svgcanvas.SvgCanvas#selectorManager
* @type {module:select.SelectorManager}
*/
const selectorManager = this.selectorManager = getSelectorManager();
/**
* @name module:svgcanvas.SvgCanvas#getNextId
* @type {module:path.EditorContext#getNextId}
*/
const getNextId = canvas.getNextId = function () {
return getCurrentDrawing().getNextId();
};
/**
* @name module:svgcanvas.SvgCanvas#getId
* @type {module:path.EditorContext#getId}
*/
const getId = canvas.getId = function () {
return getCurrentDrawing().getId();
};
/**
* The "implements" should really be an intersection applying to all types rather than a union.
* @name module:svgcanvas.SvgCanvas#call
* @type {module:draw.DrawCanvasInit#call|module:path.EditorContext#call}
*/
const call = function (ev, arg) {
if (events[ev]) {
return events[ev](window, arg);
}
return undefined;
};
const restoreRefElems = function (elem) {
// Look for missing reference elements, restore any found
const attrs = {};
refAttrs.forEach(function (item, _) {
attrs[item] = elem.getAttribute(item);
});
Object.values(attrs).forEach((val) => {
if (val && val.startsWith('url(')) {
const id = getUrlFromAttr(val).substr(1);
const ref = getElem(id);
if (!ref) {
findDefs().append(removedElements[id]);
delete removedElements[id];
}
}
});
const childs = elem.getElementsByTagName('*');
if (childs.length) {
for (let i = 0, l = childs.length; i < l; i++) {
restoreRefElems(childs[i]);
}
}
};
undoInit(
/**
* @implements {module:undo.undoContext}
*/
{
call,
restoreRefElems,
getSVGContent,
getCanvas() { return canvas; },
getCurrentMode() { return currentMode; },
getCurrentZoom,
getSVGRoot,
getSelectedElements
}
);
/**
* @name undoMgr
* @memberof module:svgcanvas.SvgCanvas#
* @type {module:history.HistoryEventHandler}
*/
const undoMgr = canvas.undoMgr = getUndoManager();
/**
* This should really be an intersection applying to all types rather than a union.
* @name module:svgcanvas~addCommandToHistory
* @type {module:path.EditorContext#addCommandToHistory|module:draw.DrawCanvasInit#addCommandToHistory}
*/
const addCommandToHistory = function (cmd) {
canvas.undoMgr.addCommandToHistory(cmd);
};
selectionInit(
/**
* @implements {module:selection.selectionContext}
*/
{
getCanvas() { return canvas; },
getDataStorage,
getCurrentGroup() { return currentGroup; },
getSelectedElements,
getSVGRoot,
getSVGContent,
getDOMContainer() { return container; },
getExtensions() { return extensions; },
setExtensions(key, value) { extensions[key] = value; },
getCurrentZoom,
getRubberBox() { return rubberBox; },
setCurBBoxes(value) { curBBoxes = value; },
getCurBBoxes(_value) { return curBBoxes; },
getCurrentResizeMode() { return currentResizeMode; },
addCommandToHistory,
getSelector() { return Selector; }
}
);
/**
* Clears the selection. The 'selected' handler is then optionally called.
* This should really be an intersection applying to all types rather than a union.
* @name module:svgcanvas.SvgCanvas#clearSelection
* @type {module:draw.DrawCanvasInit#clearSelection|module:path.EditorContext#clearSelection}
* @fires module:svgcanvas.SvgCanvas#event:selected
*/
const clearSelection = this.clearSelection = clearSelectionMethod;
/**
* Adds a list of elements to the selection. The 'selected' handler is then called.
* @name module:svgcanvas.SvgCanvas#addToSelection
* @type {module:path.EditorContext#addToSelection}
* @fires module:svgcanvas.SvgCanvas#event:selected
*/
const addToSelection = this.addToSelection = addToSelectionMethod;
/**
* @type {module:path.EditorContext#getOpacity}
*/
const getOpacity = function () {
return curShape.opacity;
};
/**
* @name module:svgcanvas.SvgCanvas#getMouseTarget
* @type {module:path.EditorContext#getMouseTarget}
*/
const getMouseTarget = this.getMouseTarget = getMouseTargetMethod;
/**
* @namespace {module:path.pathActions} pathActions
* @memberof module:svgcanvas.SvgCanvas#
* @see module:path.pathActions
*/
canvas.pathActions = pathActions;
pathModule.init(
/**
* @implements {module:path.EditorContext}
*/
{
selectorManager, // Ok since not changing
canvas, // Ok since not changing
call,
round,
clearSelection,
addToSelection,
addCommandToHistory,
remapElement,
addSVGElementFromJson,
getGridSnapping,
getOpacity,
getSelectedElements,
getContainer() {
return container;
},
setStarted(s) {
started = s;
},
getRubberBox() {
return rubberBox;
},
setRubberBox(rb) {
rubberBox = rb;
return rubberBox;
},
/**
* @param {PlainObject} ptsInfo
* @param {boolean} ptsInfo.closedSubpath
* @param {SVGCircleElement[]} ptsInfo.grips
* @fires module:svgcanvas.SvgCanvas#event:pointsAdded
* @fires module:svgcanvas.SvgCanvas#event:selected
* @returns {void}
*/
addPtsToSelection({ closedSubpath, grips }) {
// TODO: Correct this:
pathActions.canDeleteNodes = true;
pathActions.closed_subpath = closedSubpath;
call('pointsAdded', { closedSubpath, grips });
call('selected', grips);
},
/**
* @param {PlainObject} changes
* @param {ChangeElementCommand} changes.cmd
* @param {SVGPathElement} changes.elem
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
endChanges({ cmd, elem }) {
addCommandToHistory(cmd);
call('changed', [ elem ]);
},
getCurrentZoom,
getId,
getNextId,
getMouseTarget,
getCurrentMode() {
return currentMode;
},
setCurrentMode(cm) {
currentMode = cm;
return currentMode;
},
getDrawnPath() {
return drawnPath;
},
setDrawnPath(dp) {
drawnPath = dp;
return drawnPath;
},
getSVGRoot
}
);
// Interface strings, usually for title elements
const uiStrings = {};
// Animation element to change the opacity of any newly created element
const opacAni = document.createElementNS(NS.SVG, 'animate');
opacAni.setAttribute('attributeName', 'opacity');
opacAni.setAttribute('begin', 'indefinite');
opacAni.setAttribute('dur', 1);
opacAni.setAttribute('fill', 'freeze');
svgroot.appendChild(opacAni);
// (function () {
// TODO For Issue 208: this is a start on a thumbnail
// const svgthumb = svgdoc.createElementNS(NS.SVG, 'use');
// svgthumb.setAttribute('width', '100');
// svgthumb.setAttribute('height', '100');
// setHref(svgthumb, '#svgcontent');
// svgroot.append(svgthumb);
// }());
/**
* @typedef {PlainObject} module:svgcanvas.SaveOptions
* @property {boolean} apply
* @property {"embed"} [image]
* @property {Integer} round_digits
*/
// Object to contain image data for raster images that were found encodable
const encodableImages = {};
// Object with save options
/**
* @type {module:svgcanvas.SaveOptions}
*/
const saveOptions = { round_digits: 5 };
// Object with IDs for imported files, to see if one was already added
const importIds = {};
// Current text style properties
const curText = allProperties.text;
// Object to contain all included extensions
const extensions = {};
// Map of deleted reference elements
const removedElements = {};
// String with image URL of last loadable image
let lastGoodImgUrl = `${curConfig.imgPath}/logo.svg`;
// Boolean indicating whether or not a draw action has been started
let started = false;
// String with an element's initial transform attribute value
let startTransform = null;
// String indicating the current editor mode
let currentMode = 'select';
// String with the current direction in which an element is being resized
let currentResizeMode = 'none';
// Current general properties
let curProperties = curShape;
// Array with selected elements' Bounding box object
// selectedBBoxes = new Array(1),
// The DOM element that was just selected
let justSelected = null;
// DOM element for selection rectangle drawn by the user
let rubberBox = null;
// Array of current BBoxes, used in getIntersectionList().
let curBBoxes = [];
// Canvas point for the most recent right click
let lastClickPoint = null;
this.runExtension = function (name, action, vars) {
return this.runExtensions(action, vars, false, (n) => n === name);
};
/* eslint-disable max-len */
/**
* @todo Consider: Should this return an array by default, so extension results aren't overwritten?
* @todo Would be easier to document if passing in object with key of action and vars as value; could then define an interface which tied both together
* @function module:svgcanvas.SvgCanvas#runExtensions
* @param {"mouseDown"|"mouseMove"|"mouseUp"|"zoomChanged"|"IDsUpdated"|"canvasUpdated"|"toolButtonStateUpdate"|"selectedChanged"|"elementTransition"|"elementChanged"|"langReady"|"langChanged"|"addLangData"|"onNewDocument"|"workareaResized"} action
* @param {module:svgcanvas.SvgCanvas#event:ext_mouseDown|module:svgcanvas.SvgCanvas#event:ext_mouseMove|module:svgcanvas.SvgCanvas#event:ext_mouseUp|module:svgcanvas.SvgCanvas#event:ext_zoomChanged|module:svgcanvas.SvgCanvas#event:ext_IDsUpdated|module:svgcanvas.SvgCanvas#event:ext_canvasUpdated|module:svgcanvas.SvgCanvas#event:ext_toolButtonStateUpdate|module:svgcanvas.SvgCanvas#event:ext_selectedChanged|module:svgcanvas.SvgCanvas#event:ext_elementTransition|module:svgcanvas.SvgCanvas#event:ext_elementChanged|module:svgcanvas.SvgCanvas#event:ext_langReady|module:svgcanvas.SvgCanvas#event:ext_langChanged|module:svgcanvas.SvgCanvas#event:ext_addLangData|module:svgcanvas.SvgCanvas#event:ext_onNewDocument|module:svgcanvas.SvgCanvas#event:ext_workareaResized|module:svgcanvas.ExtensionVarBuilder} [vars]
* @param {boolean} [returnArray]
* @param {module:svgcanvas.ExtensionNameFilter} nameFilter
* @returns {GenericArray<module:svgcanvas.ExtensionStatus>|module:svgcanvas.ExtensionStatus|false} See {@tutorial ExtensionDocs} on the ExtensionStatus.
*/
/* eslint-enable max-len */
this.runExtensions = runExtensionsMethod;
/**
* Add an extension to the editor.
* @function module:svgcanvas.SvgCanvas#addExtension
* @param {string} name - String with the ID of the extension. Used internally; no need for i18n.
* @param {module:svgcanvas.ExtensionInitCallback} [extInitFunc] - Function supplied by the extension with its data
* @param {module:svgcanvas.ExtensionInitArgs} initArgs
* @fires module:svgcanvas.SvgCanvas#event:extension_added
* @throws {TypeError|Error} `TypeError` if `extInitFunc` is not a function, `Error`
* if extension of supplied name already exists
* @returns {Promise<void>} Resolves to `undefined`
*/
this.addExtension = async function (name, extInitFunc, { importLocale }) {
if (typeof extInitFunc !== 'function') {
throw new TypeError('Function argument expected for `svgcanvas.addExtension`');
}
if (name in extensions) {
throw new Error('Cannot add extension "' + name + '", an extension by that name already exists.');
}
// Provide private vars/funcs here. Is there a better way to do this?
/**
* @typedef {module:svgcanvas.PrivateMethods} module:svgcanvas.ExtensionArgumentObject
* @property {SVGSVGElement} svgroot See {@link module:svgcanvas~svgroot}
* @property {SVGSVGElement} svgcontent See {@link module:svgcanvas~svgcontent}
* @property {!(string|Integer)} nonce See {@link module:draw.Drawing#getNonce}
* @property {module:select.SelectorManager} selectorManager
* @property {module:SVGEditor~ImportLocale} importLocale
*/
/**
* @type {module:svgcanvas.ExtensionArgumentObject}
* @see {@link module:svgcanvas.PrivateMethods} source for the other methods/properties
*/
const argObj = canvas.mergeDeep(canvas.getPrivateMethods(), {
importLocale,
svgroot,
svgcontent,
nonce: getCurrentDrawing().getNonce(),
selectorManager
});
const extObj = await extInitFunc(argObj);
if (extObj) {
extObj.name = name;
}
extensions[name] = extObj;
return call('extension_added', extObj);
};
/**
* This method sends back an array or a NodeList full of elements that
* intersect the multi-select rubber-band-box on the currentLayer only.
*
* We brute-force `getIntersectionList` for browsers that do not support it (Firefox).
*
* Reference:
* Firefox does not implement `getIntersectionList()`, see {@link https://bugzilla.mozilla.org/show_bug.cgi?id=501421}.
* @function module:svgcanvas.SvgCanvas#getIntersectionList
* @param {SVGRect} rect
* @returns {Element[]|NodeList} Bbox elements
*/
const getIntersectionList = this.getIntersectionList = getIntersectionListMethod;
this.getStrokedBBox = getStrokedBBoxDefaultVisible;
this.getVisibleElements = getVisibleElements;
/**
* Wrap an SVG element into a group element, mark the group as 'gsvg'.
* @function module:svgcanvas.SvgCanvas#groupSvgElem
* @param {Element} elem - SVG element to wrap
* @returns {void}
*/
this.groupSvgElem = groupSvgElem;
// Set scope for these functions
// Object to contain editor event names and callback functions
const events = {};
canvas.call = call;
/**
* Array of what was changed (elements, layers).
* @event module:svgcanvas.SvgCanvas#event:changed
* @type {Element[]}
*/
/**
* Array of selected elements.
* @event module:svgcanvas.SvgCanvas#event:selected
* @type {Element[]}
*/
/**
* Array of selected elements.
* @event module:svgcanvas.SvgCanvas#event:transition
* @type {Element[]}
*/
/**
* The Element is always `SVGGElement`?
* If not `null`, will be the set current group element.
* @event module:svgcanvas.SvgCanvas#event:contextset
* @type {null|Element}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:pointsAdded
* @type {PlainObject}
* @property {boolean} closedSubpath
* @property {SVGCircleElement[]} grips Grips elements
*/
/**
* @event module:svgcanvas.SvgCanvas#event:zoomed
* @type {PlainObject}
* @property {Float} x
* @property {Float} y
* @property {Float} width
* @property {Float} height
* @property {0.5|2} factor
* @see module:SVGEditor.BBoxObjectWithFactor
*/
/**
* @event module:svgcanvas.SvgCanvas#event:updateCanvas
* @type {PlainObject}
* @property {false} center
* @property {module:math.XYObject} newCtr
*/
/**
* @typedef {PlainObject} module:svgcanvas.ExtensionInitResponsePlusName
* @implements {module:svgcanvas.ExtensionInitResponse}
* @property {string} name The extension's resolved ID (whether explicit or based on file name)
*/
/**
* Generalized extension object response of
* [`init()`]{@link module:svgcanvas.ExtensionInitCallback}
* along with the name of the extension.
* @event module:svgcanvas.SvgCanvas#event:extension_added
* @type {module:svgcanvas.ExtensionInitResponsePlusName|void}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:extensions_added
* @type {void}
*/
/**
* @typedef {PlainObject} module:svgcanvas.Message
* @property {any} data The data
* @property {string} origin The origin
*/
/**
* @event module:svgcanvas.SvgCanvas#event:message
* @type {module:svgcanvas.Message}
*/
/**
* SVG canvas converted to string.
* @event module:svgcanvas.SvgCanvas#event:saved
* @type {string}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:setnonce
* @type {!(string|Integer)}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:unsetnonce
* @type {void}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:zoomDone
* @type {void}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:cleared
* @type {void}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:exported
* @type {module:svgcanvas.ImageExportedResults}
*/
/**
* @event module:svgcanvas.SvgCanvas#event:exportedPDF
* @type {module:svgcanvas.PDFExportedResults}
*/
/* eslint-disable max-len */
/**
* Creating a cover-all class until {@link https://github.com/jsdoc3/jsdoc/issues/1545} may be supported.
* `undefined` may be returned by {@link module:svgcanvas.SvgCanvas#event:extension_added} if the extension's `init` returns `undefined` It is also the type for the following events "zoomDone", "unsetnonce", "cleared", and "extensions_added".
* @event module:svgcanvas.SvgCanvas#event:GenericCanvasEvent
* @type {module:svgcanvas.SvgCanvas#event:selected|module:svgcanvas.SvgCanvas#event:changed|module:svgcanvas.SvgCanvas#event:contextset|module:svgcanvas.SvgCanvas#event:pointsAdded|module:svgcanvas.SvgCanvas#event:extension_added|module:svgcanvas.SvgCanvas#event:extensions_added|module:svgcanvas.SvgCanvas#event:message|module:svgcanvas.SvgCanvas#event:transition|module:svgcanvas.SvgCanvas#event:zoomed|module:svgcanvas.SvgCanvas#event:updateCanvas|module:svgcanvas.SvgCanvas#event:saved|module:svgcanvas.SvgCanvas#event:exported|module:svgcanvas.SvgCanvas#event:exportedPDF|module:svgcanvas.SvgCanvas#event:setnonce|module:svgcanvas.SvgCanvas#event:unsetnonce|void}
*/
/* eslint-enable max-len */
/**
* The promise return, if present, resolves to `undefined`
* (`extension_added`, `exported`, `saved`).
* @typedef {Promise<void>|void} module:svgcanvas.EventHandlerReturn
*/
/**
* @callback module:svgcanvas.EventHandler
* @param {external:Window} win
* @param {module:svgcanvas.SvgCanvas#event:GenericCanvasEvent} arg
* @listens module:svgcanvas.SvgCanvas#event:GenericCanvasEvent
* @returns {module:svgcanvas.EventHandlerReturn}
*/
/* eslint-disable max-len */
/**
* Attaches a callback function to an event.
* @function module:svgcanvas.SvgCanvas#bind
* @param {"changed"|"contextset"|"selected"|"pointsAdded"|"extension_added"|"extensions_added"|"message"|"transition"|"zoomed"|"updateCanvas"|"zoomDone"|"saved"|"exported"|"exportedPDF"|"setnonce"|"unsetnonce"|"cleared"} ev - String indicating the name of the event
* @param {module:svgcanvas.EventHandler} f - The callback function to bind to the event
* @returns {module:svgcanvas.EventHandler} The previous event
*/
/* eslint-enable max-len */
canvas.bind = function (ev, f) {
const old = events[ev];
events[ev] = f;
return old;
};
/**
* Runs the SVG Document through the sanitizer and then updates its paths.
* @function module:svgcanvas.SvgCanvas#prepareSvg
* @param {XMLDocument} newDoc - The SVG DOM document
* @returns {void}
*/
this.prepareSvg = prepareSvg;
/**
* Removes any old rotations if present, prepends a new rotation at the
* transformed center.
* @function module:svgcanvas.SvgCanvas#setRotationAngle
* @param {string|Float} val - The new rotation angle in degrees
* @param {boolean} preventUndo - Indicates whether the action should be undoable or not
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
this.setRotationAngle = setRotationAngle;
/**
* Runs `recalculateDimensions` on the selected elements,
* adding the changes to a single batch command.
* @function module:svgcanvas.SvgCanvas#recalculateAllSelectedDimensions
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
this.recalculateAllSelectedDimensions = recalculateAllSelectedDimensions;
/**
* Debug tool to easily see the current matrix in the browser's console.
* @function module:svgcanvas~logMatrix
* @param {SVGMatrix} m The matrix
* @returns {void}
*/
const logMatrix = function (m) {
console.info([ m.a, m.b, m.c, m.d, m.e, m.f ]);
};
// Root Current Transformation Matrix in user units
let rootSctm = null;
/**
* Group: Selection.
*/
// TODO: do we need to worry about selectedBBoxes here?
/**
* Selects only the given elements, shortcut for `clearSelection(); addToSelection()`.
* @function module:svgcanvas.SvgCanvas#selectOnly
* @param {Element[]} elems - an array of DOM elements to be selected
* @param {boolean} showGrips - Indicates whether the resize grips should be shown
* @returns {void}
*/
const selectOnly = this.selectOnly = function (elems, showGrips) {
clearSelection(true);
addToSelection(elems, showGrips);
};
// TODO: could use slice here to make this faster?
// TODO: should the 'selected' handler
/**
* Removes elements from the selection.
* @function module:svgcanvas.SvgCanvas#removeFromSelection
* @param {Element[]} elemsToRemove - An array of elements to remove from selection
* @returns {void}
*/
/* const removeFromSelection = */ this.removeFromSelection = function (elemsToRemove) {
if (isNullish(selectedElements[0])) { return; }
if (!elemsToRemove.length) { return; }
// find every element and remove it from our array copy
const newSelectedItems = [];
const len = selectedElements.length;
for (let i = 0; i < len; ++i) {
const elem = selectedElements[i];
if (elem) {
// keep the item
if (!elemsToRemove.includes(elem)) {
newSelectedItems.push(elem);
} else { // remove the item and its selector
selectorManager.releaseSelector(elem);
}
}
}
// the copy becomes the master now
selectedElements = newSelectedItems;
};
/**
* Clears the selection, then adds all elements in the current layer to the selection.
* @function module:svgcanvas.SvgCanvas#selectAllInCurrentLayer
* @returns {void}
*/
this.selectAllInCurrentLayer = function () {
const currentLayer = getCurrentDrawing().getCurrentLayer();
if (currentLayer) {
currentMode = 'select';
if (currentGroup) {
selectOnly(currentGroup.children);
} else {
selectOnly(currentLayer.children);
}
}
};
let drawnPath = null;
// Mouse events
(function () {
const freehand = {
minx: null,
miny: null,
maxx: null,
maxy: null
};
const THRESHOLD_DIST = 0.8;
const STEP_COUNT = 10;
let dAttr = null;
let startX = null;
let startY = null;
let rStartX = null;
let rStartY = null;
let initBbox = {};
let sumDistance = 0;
const controllPoint2 = { x: 0, y: 0 };
const controllPoint1 = { x: 0, y: 0 };
let start = { x: 0, y: 0 };
const end = { x: 0, y: 0 };
let bSpline = { x: 0, y: 0 };
let nextPos = { x: 0, y: 0 };
let parameter;
let nextParameter;
/**
* @function eventInit Initialize from event.js
* @returns {void}
*/
eventInit(
/**
* @implements {module:event.eventContext_}
*/
{
getStarted() { return started; },
getCanvas() { return canvas; },
getDataStorage,
getCurConfig() { return curConfig; },
getCurrentMode() { return currentMode; },
getrootSctm() { return rootSctm; },
getStartX() { return startX; },
setStartX(value) { startX = value; },
getStartY() { return startY; },
setStartY(value) { startY = value; },
getRStartX() { return rStartX; },
getRStartY() { return rStartY; },
getRubberBox() { return rubberBox; },
getInitBbox() { return initBbox; },
getCurrentResizeMode() { return currentResizeMode; },
getCurrentGroup() { return currentGroup; },
getDrawnPath() { return drawnPath; },
getJustSelected() { return justSelected; },
getOpacAni() { return opacAni; },
getParameter() { return parameter; },
getNextParameter() { return nextParameter; },
getStepCount() { return STEP_COUNT; },
getThreSholdDist() { return THRESHOLD_DIST; },
getSumDistance() { return sumDistance; },
getStart(key) { return start[key]; },
getEnd(key) { return end[key]; },
getbSpline(key) { return bSpline[key]; },
getNextPos(key) { return nextPos[key]; },
getControllPoint1(key) { return controllPoint1[key]; },
getControllPoint2(key) { return controllPoint2[key]; },
getFreehand(key) { return freehand[key]; },
getDrawing() { return getCurrentDrawing(); },
getCurShape() { return curShape; },
getDAttr() { return dAttr; },
getLastGoodImgUrl() { return lastGoodImgUrl; },
getCurText(key) { return curText[key]; },
setDAttr(value) { dAttr = value; },
setEnd(key, value) { end[key] = value; },
setControllPoint1(key, value) { controllPoint1[key] = value; },
setControllPoint2(key, value) { controllPoint2[key] = value; },
setJustSelected(value) { justSelected = value; },
setParameter(value) { parameter = value; },
setStart(value) { start = value; },
setRStartX(value) { rStartX = value; },
setRStartY(value) { rStartY = value; },
setSumDistance(value) { sumDistance = value; },
setbSpline(value) { bSpline = value; },
setNextPos(value) { nextPos = value; },
setNextParameter(value) { nextParameter = value; },
setCurProperties(key, value) { curProperties[key] = value; },
setCurText(key, value) { curText[key] = value; },
setStarted(s) { started = s; },
setStartTransform(transform) { startTransform = transform; },
setCurrentMode(cm) {
currentMode = cm;
return currentMode;
},
setFreehand(key, value) { freehand[key] = value; },
setCurBBoxes(value) { curBBoxes = value; },
setRubberBox(value) { rubberBox = value; },
setInitBbox(value) { initBbox = value; },
setRootSctm(value) { rootSctm = value; },
setCurrentResizeMode(value) { currentResizeMode = value; },
setLastClickPoint(value) { lastClickPoint = value; },
getSelectedElements,
getCurrentZoom,
getId,
addCommandToHistory,
getSVGRoot,
getSVGContent,
call,
getIntersectionList
}
);
/**
* Follows these conditions:
* - When we are in a create mode, the element is added to the canvas but the
* action is not recorded until mousing up.
* - When we are in select mode, select the element, remember the position
* and do nothing else.
* @param {MouseEvent} evt
* @fires module:svgcanvas.SvgCanvas#event:ext_mouseDown
* @returns {void}
*/
const mouseDown = mouseDownEvent;
// in this function we do not record any state changes yet (but we do update
// any elements that are still being created, moved or resized on the canvas)
/**
*
* @param {MouseEvent} evt
* @fires module:svgcanvas.SvgCanvas#event:transition
* @fires module:svgcanvas.SvgCanvas#event:ext_mouseMove
* @returns {void}
*/
const mouseMove = mouseMoveEvent;
// - in create mode, the element's opacity is set properly, we create an InsertElementCommand
// and store it on the Undo stack
// - in move/resize mode, the element's attributes which were affected by the move/resize are
// identified, a ChangeElementCommand is created and stored on the stack for those attrs
// this is done in when we recalculate the selected dimensions()
/**
*
* @param {MouseEvent} evt
* @fires module:svgcanvas.SvgCanvas#event:zoomed
* @fires module:svgcanvas.SvgCanvas#event:changed
* @fires module:svgcanvas.SvgCanvas#event:ext_mouseUp
* @returns {void}
*/
const mouseUp = mouseUpEvent;
const mouseOut = mouseOutEvent;
const dblClick = dblClickEvent;
// prevent links from being followed in the canvas
const handleLinkInCanvas = function (e) {
e.preventDefault();
return false;
};
// Added mouseup to the container here.
// TODO(codedread): Figure out why after the Closure compiler, the window mouseup is ignored.
container.addEventListener('mousedown', mouseDown);
container.addEventListener('mousemove', mouseMove);
container.addEventListener('click', handleLinkInCanvas);
container.addEventListener('dblclick', dblClick);
container.addEventListener('mouseup', mouseUp);
container.addEventListener('mouseleave', mouseOut);
// TODO(rafaelcastrocouto): User preference for shift key and zoom factor
container.addEventListener('mousewheel', DOMMouseScrollEvent);
container.addEventListener('DOMMouseScroll', DOMMouseScrollEvent);
}());
textActionsInit(
/**
* @implements {module:text-actions.textActionsContext}
*/
{
getCanvas() { return canvas; },
getrootSctm() { return rootSctm; },
getSelectedElements,
getCurrentZoom,
getCurrentMode() {
return currentMode;
},
setCurrentMode(cm) {
currentMode = cm;
return currentMode;
},
getSVGRoot,
call
}
);
const textActions = canvas.textActions = textActionsMethod;
/**
* Group: Serialization.
*/
this.getSvgOption = () => { return saveOptions; };
this.setSvgOption = (key, value) => { saveOptions[key] = value; };
svgInit(
/**
* @implements {module:elem-get-set.elemInit}
*/
{
getCanvas() { return canvas; },
getDataStorage,
getSVGContent,
getSVGRoot,
getUIStrings() { return uiStrings; },
getCurrentGroup() { return currentGroup; },
getCurConfig() { return curConfig; },
getNsMap() { return nsMap; },
getSvgOption: this.getSvgOption,
setSvgOption: this.setSvgOption,
getSvgOptionApply() { return saveOptions.apply; },
getSvgOptionImages() { return saveOptions.images; },
getEncodableImages(key) { return encodableImages[key]; },
setEncodableImages(key, value) { encodableImages[key] = value; },
call,
getDOMDocument() { return svgdoc; },
getVisElems() { return visElems; },
getIdPrefix() { return idprefix; },
setCurrentZoom(value) { currentZoom = value; },
getImportIds(key) { return importIds[key]; },
setImportIds(key, value) { importIds[key] = value; },
setRemovedElements(key, value) { removedElements[key] = value; },
setSVGContent(value) { svgcontent = value; },
getrefAttrs() { return refAttrs; },
getcanvg() { return canvg; },
addCommandToHistory
}
);
/**
* Looks at DOM elements inside the `<defs>` to see if they are referred to,
* removes them from the DOM if they are not.
* @function module:svgcanvas.SvgCanvas#removeUnusedDefElems
* @returns {Integer} The number of elements that were removed
*/
this.removeUnusedDefElems = removeUnusedDefElemsMethod;
/**
* Main function to set up the SVG content for output.
* @function module:svgcanvas.SvgCanvas#svgCanvasToString
* @returns {string} The SVG image for output
*/
this.svgCanvasToString = svgCanvasToString;
/**
* Sub function ran on each SVG element to convert it to a string as desired.
* @function module:svgcanvas.SvgCanvas#svgToString
* @param {Element} elem - The SVG element to convert
* @param {Integer} indent - Number