960 lines
41 KiB
JavaScript
960 lines
41 KiB
JavaScript
/**
|
|
* Numerous tools for working with the editor's "canvas".
|
|
* @module svgcanvas
|
|
*
|
|
* @license MIT
|
|
*
|
|
* @copyright 2010 Alexis Deveria, 2010 Pavol Rusnak, 2010 Jeff Schiller, 2021 OptimistikSAS
|
|
*
|
|
*/
|
|
|
|
import { Canvg as canvg } from 'canvg';
|
|
import 'pathseg'; // SVGPathSeg Polyfill (see https://github.com/progers/pathseg)
|
|
|
|
import * as pathModule from './path.js';
|
|
import * as history from './history.js';
|
|
import * as draw from './draw.js';
|
|
import {
|
|
init as pasteInit, pasteElementsMethod
|
|
} from './paste-elem.js';
|
|
import { svgRootElement } from './svgroot.js';
|
|
import {
|
|
init as undoInit, changeSelectedAttributeNoUndoMethod,
|
|
changeSelectedAttributeMethod
|
|
} 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 * as elemGetSet 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 {
|
|
assignAttributes, cleanupElement, getElem, getUrlFromAttr,
|
|
findDefs, getHref, setHref, getRefElem, getRotationAngle,
|
|
getBBoxOfElementAsPath, convertToPath, encode64, decode64,
|
|
getVisibleElements, init as utilsInit,
|
|
getBBox as utilsGetBBox, getStrokedBBoxDefaultVisible, isNullish, blankPageObjectURL,
|
|
$id, $qa, $qq, getFeGaussianBlur, stringToHTML, insertChildAtIndex
|
|
} from './utilities.js';
|
|
import {
|
|
matrixMultiply, hasMatrixTransform, transformListToTransform
|
|
} from './math.js';
|
|
import {
|
|
convertToNum, init as unitsInit, getTypeMap
|
|
} from '../common/units.js';
|
|
import {
|
|
svgCanvasToString, svgToString, setSvgString, exportPDF, setUseDataMethod,
|
|
init as svgInit, importSvgString, embedImage, rasterExport,
|
|
uniquifyElemsMethod, removeUnusedDefElemsMethod, convertGradientsMethod
|
|
} from './svg-exec.js';
|
|
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';
|
|
|
|
import dataStorage from './dataStorage.js';
|
|
|
|
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' ];
|
|
|
|
const THRESHOLD_DIST = 0.8;
|
|
const STEP_COUNT = 10;
|
|
const CLIPBOARD_ID = 'svgedit_clipboard';
|
|
|
|
|
|
/**
|
|
* The main SvgCanvas class that manages all SVG-related functions.
|
|
* @memberof module:svgcanvas
|
|
*
|
|
*/
|
|
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) {
|
|
// imported function made available as methods
|
|
this.initializeSvgCanvasMethods();
|
|
const { pathActions } = pathModule;
|
|
|
|
// initialize class variables
|
|
this.saveOptions = { round_digits: 5 }; // Object with save options
|
|
this.importIds = {}; // Object with IDs for imported files, to see if one was already added
|
|
this.extensions = {}; // Object to contain all included extensions
|
|
this.removedElements = {}; // Map of deleted reference elements
|
|
this.started = false; // Boolean indicating whether or not a draw action has been this.started
|
|
this.startTransform = null; // String with an element's initial transform attribute value
|
|
this.currentMode = 'select'; // String indicating the current editor mode
|
|
this.currentResizeMode = 'none'; // String with the current direction in which an element is being resized
|
|
this.justSelected = null; // The DOM element that was just selected
|
|
this.rubberBox = null; // DOM element for selection rectangle drawn by the user
|
|
this.curBBoxes = []; // Array of current BBoxes, used in getIntersectionList().
|
|
this.lastClickPoint = null; // Canvas point for the most recent right click
|
|
this.events = {}; // Object to contain editor event names and callback functions
|
|
this.rootSctm = null; // Root Current Transformation Matrix in user units
|
|
this.drawnPath = null;
|
|
this.freehand = { // Mouse events
|
|
minx: null,
|
|
miny: null,
|
|
maxx: null,
|
|
maxy: null
|
|
};
|
|
this.dAttr = null;
|
|
this.startX = null;
|
|
this.startY = null;
|
|
this.rStartX = null;
|
|
this.rStartY = null;
|
|
this.initBbox = {};
|
|
this.sumDistance = 0;
|
|
this.controllPoint2 = { x: 0, y: 0 };
|
|
this.controllPoint1 = { x: 0, y: 0 };
|
|
this.start = { x: 0, y: 0 };
|
|
this.end = { x: 0, y: 0 };
|
|
this.bSpline = { x: 0, y: 0 };
|
|
this.nextPos = { x: 0, y: 0 };
|
|
this.idprefix = 'svg_'; // Prefix string for element IDs
|
|
|
|
this.curConfig = { // Default configuration options
|
|
show_outside_canvas: true,
|
|
selectNew: true,
|
|
dimensions: [ 640, 480 ]
|
|
};
|
|
// Update config with new one if given
|
|
if (config) {
|
|
this.curConfig = SvgCanvas.mergeDeep(this.curConfig, config);
|
|
}
|
|
this.lastGoodImgUrl = `${this.curConfig.imgPath}/logo.svg`; // String with image URL of last loadable image
|
|
const { dimensions } = this.curConfig; // Array with width/height of canvas
|
|
|
|
// "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.
|
|
this.svgdoc = window.document;
|
|
this.container = container;
|
|
// This is a container for the document being edited, not the document itself.
|
|
this.svgroot = svgRootElement(this.svgdoc, dimensions);
|
|
container.append(this.svgroot);
|
|
// The actual element that represents the final output SVG element.
|
|
this.svgContent = this.svgdoc.createElementNS(NS.SVG, 'svg');
|
|
clearInit(this);
|
|
this.clearSvgContentElement();
|
|
//Current `draw.Drawing` object.
|
|
this.current_drawing_ = new draw.Drawing(this.svgContent, this.idprefix);
|
|
// Float displaying the current zoom level (1 = 100%, .5 = 50%, etc.).
|
|
this.zoom = 1;
|
|
|
|
// pointer to current group (for in-group editing)
|
|
this.currentGroup = null;
|
|
|
|
// Object containing data for the currently selected styles
|
|
const allProperties = {
|
|
shape: {
|
|
fill: (this.curConfig.initFill.color === 'none' ? '' : '#') + this.curConfig.initFill.color,
|
|
fill_paint: null,
|
|
fill_opacity: this.curConfig.initFill.opacity,
|
|
stroke: '#' + this.curConfig.initStroke.color,
|
|
stroke_paint: null,
|
|
stroke_opacity: this.curConfig.initStroke.opacity,
|
|
stroke_width: this.curConfig.initStroke.width,
|
|
stroke_dasharray: 'none',
|
|
stroke_linejoin: 'miter',
|
|
stroke_linecap: 'butt',
|
|
opacity: this.curConfig.initOpacity
|
|
}
|
|
};
|
|
allProperties.text = SvgCanvas.mergeDeep({}, allProperties.shape);
|
|
allProperties.text = SvgCanvas.mergeDeep(allProperties.text, {
|
|
fill: '#000000',
|
|
stroke_width: this.curConfig.text && this.curConfig.text.stroke_width,
|
|
font_size: this.curConfig.text && this.curConfig.text.font_size,
|
|
font_family: this.curConfig.text && this.curConfig.text.font_family
|
|
});
|
|
this.curText = allProperties.text; // Current text style properties
|
|
|
|
// Current shape style properties
|
|
this.curShape = allProperties.shape;
|
|
this.curProperties = this.curShape; // Current general properties
|
|
|
|
// Array with all the currently selected elements
|
|
// default size of 1 until it needs to grow bigger
|
|
this.selectedElements = [];
|
|
|
|
jsonInit(this);
|
|
unitsInit(this);
|
|
utilsInit(this);
|
|
coordsInit(this);
|
|
recalculateInit(this);
|
|
selectInit(this);
|
|
undoInit(this);
|
|
selectionInit(this);
|
|
|
|
this.nsMap = getReverseNS();
|
|
this.selectorManager = getSelectorManager();
|
|
|
|
this.pathActions = pathActions;
|
|
pathModule.init(this);
|
|
// Interface strings, usually for title elements
|
|
this.uiStrings = {};
|
|
|
|
// Animation element to change the opacity of any newly created element
|
|
this.opacAni = document.createElementNS(NS.SVG, 'animate');
|
|
this.opacAni.setAttribute('attributeName', 'opacity');
|
|
this.opacAni.setAttribute('begin', 'indefinite');
|
|
this.opacAni.setAttribute('dur', 1);
|
|
this.opacAni.setAttribute('fill', 'freeze');
|
|
this.svgroot.appendChild(this.opacAni);
|
|
|
|
eventInit(this);
|
|
textActionsInit(this);
|
|
svgInit(this);
|
|
draw.init(this);
|
|
elemGetSet.init(this);
|
|
|
|
// prevent links from being followed in the canvas
|
|
const handleLinkInCanvas = function (e) {
|
|
e.preventDefault();
|
|
return false;
|
|
};
|
|
container.addEventListener('mousedown', mouseDownEvent);
|
|
container.addEventListener('mousemove', mouseMoveEvent);
|
|
container.addEventListener('click', handleLinkInCanvas);
|
|
container.addEventListener('dblclick', dblClickEvent);
|
|
container.addEventListener('mouseup', mouseUpEvent);
|
|
container.addEventListener('mouseleave', mouseOutEvent);
|
|
container.addEventListener('mousewheel', DOMMouseScrollEvent);
|
|
container.addEventListener('DOMMouseScroll', DOMMouseScrollEvent);
|
|
|
|
// Alias function
|
|
this.linkControlPoints = pathActions.linkControlPoints;
|
|
this.curCommand = null;
|
|
this.filter = null;
|
|
this.filterHidden = false;
|
|
|
|
blurInit(this);
|
|
selectedElemInit(this);
|
|
|
|
|
|
/**
|
|
* Transfers sessionStorage from one tab to another.
|
|
* @param {!Event} ev Storage event.
|
|
* @returns {void}
|
|
*/
|
|
const storageChange = (ev) => {
|
|
if (!ev.newValue) return; // This is a call from removeItem.
|
|
if (ev.key === CLIPBOARD_ID + '_startup') {
|
|
// Another tab asked for our sessionStorage.
|
|
localStorage.removeItem(CLIPBOARD_ID + '_startup');
|
|
this.flashStorage();
|
|
} else if (ev.key === CLIPBOARD_ID) {
|
|
// Another tab sent data.
|
|
sessionStorage.setItem(CLIPBOARD_ID, ev.newValue);
|
|
}
|
|
};
|
|
|
|
// Listen for changes to localStorage.
|
|
window.addEventListener('storage', storageChange, false);
|
|
// Ask other tabs for sessionStorage (this is ONLY to trigger event).
|
|
localStorage.setItem(CLIPBOARD_ID + '_startup', Math.random());
|
|
|
|
pasteInit(this);
|
|
|
|
this.contentW = this.getResolution().w;
|
|
this.contentH = this.getResolution().h;
|
|
this.clear();
|
|
} // End constructor
|
|
getSvgOption() { return this.saveOptions; }
|
|
setSvgOption(key, value) { this.saveOptions[key] = value; }
|
|
getSelectedElements() { return this.selectedElements; }
|
|
setSelectedElements(key, value) { this.selectedElements[key] = value; }
|
|
setEmptySelectedElements() { this.selectedElements = []; }
|
|
getSvgRoot() { return this.svgroot; }
|
|
getDOMDocument() { return this.svgdoc; }
|
|
getDOMContainer() { return this.container; }
|
|
getCurConfig() { return this.curConfig; }
|
|
setIdPrefix(p) { this.idprefix = p; }
|
|
getCurrentDrawing() { return this.current_drawing_;}
|
|
getCurShape() { return this.curShape; }
|
|
getCurrentGroup() { return this.currentGroup; }
|
|
getBaseUnit() { return this.curConfig.baseUnit; }
|
|
getHeight() { return this.svgContent.getAttribute('height') / this.zoom; }
|
|
getWidth() { return this.svgContent.getAttribute('width') / this.zoom; }
|
|
getRoundDigits() { return this.saveOptions.round_digits; }
|
|
getSnappingStep() { return this.curConfig.snappingStep; }
|
|
getGridSnapping() { return this.curConfig.gridSnapping; }
|
|
getStartTransform() { return this.startTransform; }
|
|
setStartTransform(transform) { this.startTransform = transform; }
|
|
getZoom () { return this.zoom; }
|
|
round(val) { return Number.parseInt(val * this.zoom) / this.zoom; }
|
|
createSVGElement(jsonMap) { return this.addSVGElemensFromJson(jsonMap); }
|
|
getContainer() { return this.container; }
|
|
setStarted(s) { this.started = s; }
|
|
getRubberBox() { return this.rubberBox; }
|
|
setRubberBox(rb) {
|
|
this.rubberBox = rb;
|
|
return this.rubberBox;
|
|
}
|
|
addPtsToSelection({ closedSubpath, grips }) {
|
|
// TODO: Correct this:
|
|
this.pathActions.canDeleteNodes = true;
|
|
this.pathActions.closed_subpath = closedSubpath;
|
|
this.call('pointsAdded', { closedSubpath, grips });
|
|
this.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 }) {
|
|
this.addCommandToHistory(cmd);
|
|
this.call('changed', [ elem ]);
|
|
}
|
|
getCurrentMode() { return this.currentMode; }
|
|
setCurrentMode(cm) {
|
|
this.currentMode = cm;
|
|
return this.currentMode;
|
|
}
|
|
getDrawnPath() { return this.drawnPath; }
|
|
setDrawnPath(dp) {
|
|
this.drawnPath = dp;
|
|
return this.drawnPath;
|
|
}
|
|
setCurrentGroup(cg) { this.currentGroup = cg; }
|
|
changeSvgContent() { this.call('changed', [ this.svgContent ]); }
|
|
getStarted() { return this.started; }
|
|
getCanvas() { return this; }
|
|
getrootSctm() { return this.rootSctm; }
|
|
getStartX() { return this.startX; }
|
|
setStartX(value) { this.startX = value; }
|
|
getStartY() { return this.startY; }
|
|
setStartY(value) { this.startY = value; }
|
|
getRStartX() { return this.rStartX; }
|
|
getRStartY() { return this.rStartY; }
|
|
getInitBbox() { return this.initBbox; }
|
|
getCurrentResizeMode() { return this.currentResizeMode; }
|
|
getJustSelected() { return this.justSelected; }
|
|
getOpacAni() { return this.opacAni; }
|
|
getParameter() { return this.parameter; }
|
|
getNextParameter() { return this.nextParameter; }
|
|
getStepCount() { return STEP_COUNT; }
|
|
getThreSholdDist() { return THRESHOLD_DIST; }
|
|
getSumDistance() { return this.sumDistance; }
|
|
getStart(key) { return this.start[key]; }
|
|
getEnd(key) { return this.end[key]; }
|
|
getbSpline(key) { return this.bSpline[key]; }
|
|
getNextPos(key) { return this.nextPos[key]; }
|
|
getControllPoint1(key) { return this.controllPoint1[key]; }
|
|
getControllPoint2(key) { return this.controllPoint2[key]; }
|
|
getFreehand(key) { return this.freehand[key]; }
|
|
getDrawing() { return this.getCurrentDrawing(); }
|
|
getDAttr() { return this.dAttr; }
|
|
getLastGoodImgUrl() { return this.lastGoodImgUrl; }
|
|
getCurText(key) { return this.curText[key]; }
|
|
setDAttr(value) { this.dAttr = value; }
|
|
setEnd(key, value) { this.end[key] = value; }
|
|
setControllPoint1(key, value) { this.controllPoint1[key] = value; }
|
|
setControllPoint2(key, value) { this.controllPoint2[key] = value; }
|
|
setJustSelected(value) { this.justSelected = value; }
|
|
setParameter(value) { this.parameter = value; }
|
|
setStart(value) { this.start = value; }
|
|
setRStartX(value) { this.rStartX = value; }
|
|
setRStartY(value) { this.rStartY = value; }
|
|
setSumDistance(value) { this.sumDistance = value; }
|
|
setbSpline(value) { this.bSpline = value; }
|
|
setNextPos(value) { this.nextPos = value; }
|
|
setNextParameter(value) { this.nextParameter = value; }
|
|
setCurProperties(key, value) { this.curProperties[key] = value; }
|
|
setCurText(key, value) { this.curText[key] = value; }
|
|
setFreehand(key, value) { this.freehand[key] = value; }
|
|
setCurBBoxes(value) { this.curBBoxes = value; }
|
|
getCurBBoxes() { return this.curBBoxes; }
|
|
setInitBbox(value) { this.initBbox = value; }
|
|
setRootSctm(value) { this.rootSctm = value; }
|
|
setCurrentResizeMode(value) { this.currentResizeMode = value; }
|
|
getLastClickPoint(key) { return this.lastClickPoint[key]; }
|
|
setLastClickPoint(value) { this.lastClickPoint = value; }
|
|
getId() { return this.getCurrentDrawing().getId(); }
|
|
getUIStrings() { return this.uiStrings; }
|
|
getNsMap() { return this.nsMap; }
|
|
getSvgOptionApply() { return this.saveOptions.apply; }
|
|
getSvgOptionImages() { return this.saveOptions.images; }
|
|
getEncodableImages(key) { return this.encodableImages[key]; }
|
|
setEncodableImages(key, value) { this.encodableImages[key] = value; }
|
|
getVisElems() { return visElems; }
|
|
getIdPrefix() { return this.idprefix; }
|
|
getDataStorage() { return dataStorage; }
|
|
setZoom(value) { this.zoom = value; }
|
|
getImportIds(key) { return this.importIds[key]; }
|
|
setImportIds(key, value) { this.importIds[key] = value; }
|
|
setRemovedElements(key, value) { this.removedElements[key] = value; }
|
|
setSvgContent(value) { this.svgContent = value; }
|
|
getrefAttrs() { return refAttrs; }
|
|
getcanvg() { return canvg; }
|
|
setCanvas(key, value) { this[key] = value; }
|
|
getCurProperties(key) { return this.curProperties[key]; }
|
|
setCurShape(key, value) { this.curShape[key] = value; }
|
|
gettingSelectorManager() { return this.selectorManager; }
|
|
getContentW() { return this.contentW; }
|
|
getContentH() { return this.contentH; }
|
|
getClipboardID() { return CLIPBOARD_ID; }
|
|
getSvgContent() { return this.svgContent; }
|
|
getExtensions() { return this.extensions; }
|
|
getSelector() { return Selector; }
|
|
getMode() { return this.currentMode; } // The current editor mode string
|
|
getNextId() { return this.getCurrentDrawing().getNextId(); }
|
|
getCurCommand() { return this.curCommand; }
|
|
setCurCommand(value) { this.curCommand = value; }
|
|
getFilter() { return this.filter; }
|
|
setFilter(value) { this.filter = value; }
|
|
getFilterHidden() { return this.filterHidden; }
|
|
setFilterHidden(value) { this.filterHidden = value; }
|
|
/**
|
|
* Sets the editor's mode to the given string.
|
|
* @function module:svgcanvas.SvgCanvas#setMode
|
|
* @param {string} name - String with the new mode to change to
|
|
* @returns {void}
|
|
*/
|
|
setMode(name) {
|
|
this.pathActions.clear(true);
|
|
this.textActions.clear();
|
|
this.curProperties = (this.selectedElements[0] && this.selectedElements[0].nodeName === 'text') ? this.curText : this.curShape;
|
|
this.currentMode = name;
|
|
}
|
|
/**
|
|
* Clears the current document. This is not an undoable action.
|
|
* @function module:svgcanvas.SvgCanvas#clear
|
|
* @fires module:svgcanvas.SvgCanvas#event:cleared
|
|
* @returns {void}
|
|
*/
|
|
clear() {
|
|
this.pathActions.clear();
|
|
this.clearSelection();
|
|
// clear the svgcontent node
|
|
this.clearSvgContentElement();
|
|
// create new document
|
|
this.current_drawing_ = new draw.Drawing(this.svgContent);
|
|
// create empty first layer
|
|
this.createLayer('Layer 1');
|
|
// clear the undo stack
|
|
this.undoMgr.resetUndoStack();
|
|
// reset the selector manager
|
|
this.selectorManager.initGroup();
|
|
// reset the rubber band box
|
|
this.rubberBox = this.selectorManager.getRubberBandBox();
|
|
this.call('cleared');
|
|
}
|
|
runExtension (name, action, vars) {
|
|
return this.runExtensions(action, vars, false, (n) => n === name);
|
|
}
|
|
async addExtension (name, extInitFunc, { importLocale }) {
|
|
if (typeof extInitFunc !== 'function') {
|
|
throw new TypeError('Function argument expected for `svgcanvas.addExtension`');
|
|
}
|
|
if (name in this.extensions) {
|
|
throw new Error('Cannot add extension "' + name + '", an extension by that name already exists.');
|
|
}
|
|
const argObj = {
|
|
importLocale,
|
|
svgroot: this.svgroot,
|
|
svgContent: this.svgContent,
|
|
nonce: this.getCurrentDrawing().getNonce(),
|
|
selectorManager: this.selectorManager
|
|
};
|
|
const extObj = await extInitFunc(argObj);
|
|
if (extObj) {
|
|
extObj.name = name;
|
|
}
|
|
this.extensions[name] = extObj;
|
|
return this.call('extension_added', extObj);
|
|
}
|
|
addCommandToHistory(cmd) { this.undoMgr.addCommandToHistory(cmd); }
|
|
restoreRefElements(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(this.removedElements[id]);
|
|
delete this.removedElements[id];
|
|
}
|
|
}
|
|
});
|
|
const childs = elem.getElementsByTagName('*');
|
|
|
|
if (childs.length) {
|
|
for (let i = 0, l = childs.length; i < l; i++) {
|
|
this.restoreRefElements(childs[i]);
|
|
}
|
|
}
|
|
}
|
|
call(ev, arg) {
|
|
if (this.events[ev]) {
|
|
return this.events[ev](window, arg);
|
|
}
|
|
return undefined;
|
|
}
|
|
/**
|
|
* Attaches a callback function to an event.
|
|
* @function module:svgcanvas.SvgCanvas#bind
|
|
* @param {string} 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
|
|
*/
|
|
bind (ev, f) {
|
|
const old = this.events[ev];
|
|
this.events[ev] = f;
|
|
return old;
|
|
}
|
|
/**
|
|
* Flash the clipboard data momentarily on localStorage so all tabs can see.
|
|
* @returns {void}
|
|
*/
|
|
flashStorage() {
|
|
const data = sessionStorage.getItem(CLIPBOARD_ID);
|
|
localStorage.setItem(CLIPBOARD_ID, data);
|
|
setTimeout(function () {
|
|
localStorage.removeItem(CLIPBOARD_ID);
|
|
}, 1);
|
|
}
|
|
/**
|
|
* 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}
|
|
*/
|
|
selectOnly(elems, showGrips) {
|
|
this.clearSelection(true);
|
|
this.addToSelection(elems, showGrips);
|
|
}
|
|
/**
|
|
* Removes elements from the selection.
|
|
* @function module:svgcanvas.SvgCanvas#removeFromSelection
|
|
* @param {Element[]} elemsToRemove - An array of elements to remove from selection
|
|
* @returns {void}
|
|
*/
|
|
removeFromSelection(elemsToRemove) {
|
|
if (isNullish(this.selectedElements[0])) { return; }
|
|
if (!elemsToRemove.length) { return; }
|
|
|
|
// find every element and remove it from our array copy
|
|
const newSelectedItems = [];
|
|
const len = this.selectedElements.length;
|
|
for (let i = 0; i < len; ++i) {
|
|
const elem = this.selectedElements[i];
|
|
if (elem) {
|
|
// keep the item
|
|
if (!elemsToRemove.includes(elem)) {
|
|
newSelectedItems.push(elem);
|
|
} else { // remove the item and its selector
|
|
this.selectorManager.releaseSelector(elem);
|
|
}
|
|
}
|
|
}
|
|
// the copy becomes the master now
|
|
this.selectedElements = newSelectedItems;
|
|
}
|
|
/**
|
|
* Clears the selection, then adds all elements in the current layer to the selection.
|
|
* @function module:svgcanvas.SvgCanvas#selectAllInCurrentLayer
|
|
* @returns {void}
|
|
*/
|
|
selectAllInCurrentLayer() {
|
|
const currentLayer = this.getCurrentDrawing().getCurrentLayer();
|
|
if (currentLayer) {
|
|
this.currentMode = 'select';
|
|
if (this.currentGroup) {
|
|
this.selectOnly(this.currentGroup.children);
|
|
} else {
|
|
this.selectOnly(currentLayer.children);
|
|
}
|
|
}
|
|
}
|
|
getOpacity() {
|
|
return this.curShape.opacity;
|
|
}
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#getSnapToGrid
|
|
* @returns {boolean} The current snap to grid setting
|
|
*/
|
|
getSnapToGrid() { return this.curConfig.gridSnapping; }
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#getVersion
|
|
* @returns {string} A string which describes the revision number of SvgCanvas.
|
|
*/
|
|
getVersion() { return 'svgcanvas.js ($Rev$)'; }
|
|
/**
|
|
* Update interface strings with given values.
|
|
* @function module:svgcanvas.SvgCanvas#setUiStrings
|
|
* @param {module:path.uiStrings} strs - Object with strings (see the [locales API]{@link module:locale.LocaleStrings} and the [tutorial]{@tutorial LocaleDocs})
|
|
* @returns {void}
|
|
*/
|
|
setUiStrings(strs) {
|
|
Object.assign(this.uiStrings, strs.notification);
|
|
pathModule.setUiStrings(strs);
|
|
}
|
|
/**
|
|
* Update configuration options with given values.
|
|
* @function module:svgcanvas.SvgCanvas#setConfig
|
|
* @param {module:SVGEditor.Config} opts - Object with options
|
|
* @returns {void}
|
|
*/
|
|
setConfig(opts) { Object.assign(this.curConfig, opts); }
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#getDocumentTitle
|
|
* @returns {string|void} The current document title or an empty string if not found
|
|
*/
|
|
getDocumentTitle() { return this.getTitle(this.svgContent); }
|
|
getOffset() {
|
|
return { x: Number(this.svgContent.getAttribute('x')), y: Number(this.svgContent.getAttribute('y')) };
|
|
}
|
|
getColor(type) { return this.curProperties[type]; }
|
|
setStrokePaint(paint) { this.setPaint('stroke', paint); }
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#setFillPaint
|
|
* @param {module:jGraduate~Paint} paint
|
|
* @returns {void}
|
|
*/
|
|
setFillPaint(paint) { this.setPaint('fill', paint); }
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#getStrokeWidth
|
|
* @returns {Float|string} The current stroke-width value
|
|
*/
|
|
getStrokeWidth() { return this.curProperties.stroke_width; }
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#getStyle
|
|
* @returns {module:svgcanvas.StyleOptions} current style options
|
|
*/
|
|
getStyle() { return this.curShape; }
|
|
/**
|
|
* Sets the given opacity on the current selected elements.
|
|
* @function module:svgcanvas.SvgCanvas#setOpacity
|
|
* @param {string} val
|
|
* @returns {void}
|
|
*/
|
|
setOpacity(val) {
|
|
this.curShape.opacity = val;
|
|
this.changeSelectedAttribute('opacity', val);
|
|
}
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#getFillOpacity
|
|
* @returns {Float} the current fill opacity
|
|
*/
|
|
getFillOpacity() { return this.curShape.fill_opacity; }
|
|
/**
|
|
* @function module:svgcanvas.SvgCanvas#getStrokeOpacity
|
|
* @returns {string} the current stroke opacity
|
|
*/
|
|
getStrokeOpacity() { return this.curShape.stroke_opacity; }
|
|
/**
|
|
* Sets the current fill/stroke opacity.
|
|
* @function module:svgcanvas.SvgCanvas#setPaintOpacity
|
|
* @param {string} type - String with "fill" or "stroke"
|
|
* @param {Float} val - Float with the new opacity value
|
|
* @param {boolean} preventUndo - Indicates whether or not this should be an undoable action
|
|
* @returns {void}
|
|
*/
|
|
setPaintOpacity(type, val, preventUndo) {
|
|
this.curShape[type + '_opacity'] = val;
|
|
if (!preventUndo) {
|
|
this.changeSelectedAttribute(type + '-opacity', val);
|
|
} else {
|
|
this.changeSelectedAttributeNoUndo(type + '-opacity', val);
|
|
}
|
|
}
|
|
/**
|
|
* Gets the current fill/stroke opacity.
|
|
* @function module:svgcanvas.SvgCanvas#getPaintOpacity
|
|
* @param {"fill"|"stroke"} type - String with "fill" or "stroke"
|
|
* @returns {Float} Fill/stroke opacity
|
|
*/
|
|
getPaintOpacity(type) {
|
|
return type === 'fill' ? this.getFillOpacity() : this.getStrokeOpacity();
|
|
}
|
|
/**
|
|
* Gets the `stdDeviation` blur value of the given element.
|
|
* @function module:svgcanvas.SvgCanvas#getBlur
|
|
* @param {Element} elem - The element to check the blur value for
|
|
* @returns {string} stdDeviation blur attribute value
|
|
*/
|
|
getBlur(elem) {
|
|
let val = 0;
|
|
if (elem) {
|
|
const filterUrl = elem.getAttribute('filter');
|
|
if (filterUrl) {
|
|
const blur = getElem(elem.id + '_blur');
|
|
if (blur) {
|
|
val = blur.firstChild.getAttribute('stdDeviation');
|
|
} else {
|
|
const filterElem = getRefElem(filterUrl);
|
|
const blurElem = getFeGaussianBlur(filterElem);
|
|
if (blurElem !== null) {
|
|
val = blurElem.getAttribute('stdDeviation');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return val;
|
|
}
|
|
/**
|
|
* Sets a given URL to be a "last good image" URL.
|
|
* @function module:svgcanvas.SvgCanvas#setGoodImage
|
|
* @param {string} val
|
|
* @returns {void}
|
|
*/
|
|
setGoodImage(val) { this.lastGoodImgUrl = val; }
|
|
/**
|
|
* Returns the current drawing as raw SVG XML text.
|
|
* @function module:svgcanvas.SvgCanvas#getSvgString
|
|
* @returns {string} The current drawing as raw SVG XML text.
|
|
*/
|
|
getSvgString () {
|
|
this.saveOptions.apply = false;
|
|
return this.svgCanvasToString();
|
|
}
|
|
/**
|
|
* This function determines whether to use a nonce in the prefix, when
|
|
* generating IDs for future documents in SVG-Edit.
|
|
* If you're controlling SVG-Edit externally, and want randomized IDs, call
|
|
* this BEFORE calling `svgCanvas.setSvgString`.
|
|
* @function module:svgcanvas.SvgCanvas#randomizeIds
|
|
* @param {boolean} [enableRandomization] If true, adds a nonce to the prefix. Thus
|
|
* `svgCanvas.randomizeIds() <==> svgCanvas.randomizeIds(true)`
|
|
* @returns {void}
|
|
*/
|
|
randomizeIds (enableRandomization) {
|
|
if (arguments.length > 0 && enableRandomization === false) {
|
|
draw.randomizeIds(false, this.getCurrentDrawing());
|
|
} else {
|
|
draw.randomizeIds(true, this.getCurrentDrawing());
|
|
}
|
|
}
|
|
/**
|
|
* Convert selected element to a path, or get the BBox of an element-as-path.
|
|
* @function module:svgcanvas.SvgCanvas#convertToPath
|
|
* @todo (codedread): Remove the getBBox argument and split this function into two.
|
|
* @param {Element} elem - The DOM element to be converted
|
|
* @param {boolean} getBBox - Boolean on whether or not to only return the path's BBox
|
|
* @returns {void|DOMRect|false|SVGPathElement|null} If the getBBox flag is true, the resulting path's bounding box object.
|
|
* Otherwise the resulting path element is returned.
|
|
*/
|
|
convertToPath(elem, getBBox) {
|
|
if (isNullish(elem)) {
|
|
const elems = this.selectedElements;
|
|
elems.forEach((el) => {
|
|
if (el) { this.convertToPath(el); }
|
|
});
|
|
return undefined;
|
|
}
|
|
if (getBBox) {
|
|
return getBBoxOfElementAsPath(elem, this.addSVGElemensFromJson, this.pathActions);
|
|
}
|
|
// TODO: Why is this applying attributes from this.curShape, then inside utilities.convertToPath it's pulling addition attributes from elem?
|
|
// TODO: If convertToPath is called with one elem, this.curShape and elem are probably the same; but calling with multiple is a bug or cool feature.
|
|
const attrs = {
|
|
fill: this.curShape.fill,
|
|
'fill-opacity': this.curShape.fill_opacity,
|
|
stroke: this.curShape.stroke,
|
|
'stroke-width': this.curShape.stroke_width,
|
|
'stroke-dasharray': this.curShape.stroke_dasharray,
|
|
'stroke-linejoin': this.curShape.stroke_linejoin,
|
|
'stroke-linecap': this.curShape.stroke_linecap,
|
|
'stroke-opacity': this.curShape.stroke_opacity,
|
|
opacity: this.curShape.opacity,
|
|
visibility: 'hidden'
|
|
};
|
|
return convertToPath(
|
|
elem, attrs, this.addSVGElemensFromJson, this.pathActions,
|
|
this.clearSelection, this.addToSelection, history, this.addCommandToHistory
|
|
);
|
|
}
|
|
/**
|
|
* Removes all selected elements from the DOM and adds the change to the
|
|
* history stack. Remembers removed elements on the clipboard.
|
|
* @function module:svgcanvas.SvgCanvas#cutSelectedElements
|
|
* @returns {void}
|
|
*/
|
|
cutSelectedElements() {
|
|
this.copySelectedElements();
|
|
this.deleteSelectedElements();
|
|
}
|
|
initializeSvgCanvasMethods() {
|
|
this.getJsonFromSvgElements = getJsonFromSvgElements;
|
|
this.addSVGElemensFromJson = addSVGElementsFromJson;
|
|
this.clearSvgContentElement = clearSvgContentElementInit;
|
|
this.textActions = textActionsMethod;
|
|
this.getIntersectionList = getIntersectionListMethod;
|
|
this.getStrokedBBox = getStrokedBBoxDefaultVisible;
|
|
this.getVisibleElements = getVisibleElements;
|
|
this.uniquifyElems = uniquifyElemsMethod;
|
|
this.setUseData = setUseDataMethod;
|
|
this.convertGradients = convertGradientsMethod;
|
|
this.setSvgString = setSvgString;
|
|
this.importSvgString = importSvgString;
|
|
this.runExtensions = runExtensionsMethod;
|
|
this.clearSelection = clearSelectionMethod;
|
|
this.addToSelection = addToSelectionMethod;
|
|
this.stringToHTML = stringToHTML;
|
|
this.insertChildAtIndex = insertChildAtIndex;
|
|
this.getClosest = getClosest;
|
|
this.getParents = getParents;
|
|
this.isLayer = draw.Layer.isLayer;
|
|
this.matrixMultiply = matrixMultiply;
|
|
this.hasMatrixTransform = hasMatrixTransform;
|
|
this.transformListToTransform = transformListToTransform;
|
|
this.convertToNum = convertToNum;
|
|
this.findDefs = findDefs;
|
|
this.getUrlFromAttr = getUrlFromAttr;
|
|
this.getHref = getHref;
|
|
this.setHref = setHref;
|
|
this.getBBox = utilsGetBBox;
|
|
this.getRotationAngle = getRotationAngle;
|
|
this.getElem = getElem;
|
|
this.getRefElem = getRefElem;
|
|
this.assignAttributes = assignAttributes;
|
|
this.cleanupElement = cleanupElement;
|
|
this.remapElement = remapElement;
|
|
this.recalculateDimensions = recalculateDimensions;
|
|
this.sanitizeSvg = sanitizeSvg;
|
|
this.groupSvgElem = groupSvgElem; // Wrap an SVG element into a group element, mark the group as 'gsvg'.
|
|
this.prepareSvg = prepareSvg; //Runs the SVG Document through the sanitizer and then updates its paths.
|
|
this.setRotationAngle = setRotationAngle; // Removes any old rotations if present, prepends a new rotation at the transformed center.
|
|
this.recalculateAllSelectedDimensions = recalculateAllSelectedDimensions; // Runs `recalculateDimensions` on selected elements,adding changes to a single batch command.
|
|
this.copySelectedElements = copySelectedElements;
|
|
this.pasteElements = pasteElementsMethod; // Remembers the current selected elements on the clipboard.
|
|
this.groupSelectedElements = groupSelectedElements; // Wraps all the selected elements in a group (`g`) element.
|
|
this.pushGroupProperties = pushGroupProperty; // Pushes all appropriate parent group properties down to its children
|
|
this.ungroupSelectedElement = ungroupSelectedElement; // Unwraps all the elements in a selected group (`g`) element
|
|
this.moveToTopSelectedElement = moveToTopSelectedElem; // Repositions the selected element to the bottom in the DOM to appear on top
|
|
this.moveToBottomSelectedElement = moveToBottomSelectedElem; // Repositions the selected element to the top in the DOM to appear under other elements
|
|
this.moveUpDownSelected = moveUpDownSelected; // Moves the select element up or down the stack, based on the visibly
|
|
this.moveSelectedElements = moveSelectedElements; // Moves selected elements on the X/Y axis.
|
|
this.cloneSelectedElements = cloneSelectedElements; // Create deep DOM copies (clones) of all selected elements and move them slightly
|
|
this.alignSelectedElements = alignSelectedElements; // Aligns selected elements.
|
|
this.updateCanvas = updateCanvas; // Updates the editor canvas width/height/position after a zoom has occurred.
|
|
this.cycleElement = cycleElement; // Select the next/previous element within the current layer.
|
|
this.getMouseTarget = getMouseTargetMethod;
|
|
this.removeUnusedDefElems = removeUnusedDefElemsMethod; //remove DOM elements inside the `<defs>` if they are notreferred to,
|
|
this.svgCanvasToString = svgCanvasToString; // Main function to set up the SVG content for output.
|
|
this.svgToString = svgToString; // Sub function ran on each SVG element to convert it to a string as desired.
|
|
this.embedImage = embedImage; // Converts a given image file to a data URL when possibl
|
|
this.rasterExport = rasterExport; // Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image
|
|
this.exportPDF = exportPDF; // Generates a PDF based on the current image, then calls "exportedPDF"
|
|
this.identifyLayers = draw.identifyLayers;
|
|
this.createLayer = draw.createLayer;
|
|
this.cloneLayer = draw.cloneLayer;
|
|
this.deleteCurrentLayer = draw.deleteCurrentLayer;
|
|
this.setCurrentLayer = draw.setCurrentLayer;
|
|
this.renameCurrentLayer = draw.renameCurrentLayer;
|
|
this.setCurrentLayerPosition = draw.setCurrentLayerPosition;
|
|
this.setLayerVisibility = draw.setLayerVisibility;
|
|
this.moveSelectedToLayer = draw.moveSelectedToLayer;
|
|
this.mergeLayer = draw.mergeLayer;
|
|
this.mergeAllLayers = draw.mergeAllLayers;
|
|
this.leaveContext = draw.leaveContext;
|
|
this.setContext = draw.setContext;
|
|
this.getBold = elemGetSet.getBoldMethod; // Check whether selected element is bold or not.
|
|
this.setBold = elemGetSet.setBoldMethod; // Make the selected element bold or normal.
|
|
this.getItalic = elemGetSet.getItalicMethod; // Check whether selected element is in italics or not.
|
|
this.setItalic = elemGetSet.setItalicMethod; // Make the selected element italic or normal.
|
|
this.setTextAnchor = elemGetSet.setTextAnchorMethod; // Set the new text anchor.
|
|
this.getFontFamily = elemGetSet.getFontFamilyMethod; // The current font family
|
|
this.setFontFamily = elemGetSet.setFontFamilyMethod; // Set the new font family.
|
|
this.setFontColor = elemGetSet.setFontColorMethod; // Set the new font color.
|
|
this.getFontColor = elemGetSet.getFontColorMethod; // The current font color
|
|
this.getFontSize = elemGetSet.getFontSizeMethod; // The current font size
|
|
this.setFontSize = elemGetSet.setFontSizeMethod; // Applies the given font size to the selected element.
|
|
this.getText = elemGetSet.getTextMethod; // current text (`textContent`) of the selected element
|
|
this.setTextContent = elemGetSet.setTextContentMethod; // Updates the text element with the given string.
|
|
this.setImageURL = elemGetSet.setImageURLMethod; // Sets the new image URL for the selected image element
|
|
this.setLinkURL = elemGetSet.setLinkURLMethod; // Sets the new link URL for the selected anchor element.
|
|
this.setRectRadius = elemGetSet.setRectRadiusMethod; // Sets the `rx` and `ry` values to the selected `rect` element
|
|
this.makeHyperlink = elemGetSet.makeHyperlinkMethod; // Wraps the selected element(s) in an anchor element or converts group to one.
|
|
this.removeHyperlink = elemGetSet.removeHyperlinkMethod;
|
|
this.setSegType = elemGetSet.setSegTypeMethod; // Sets the new segment type to the selected segment(s).
|
|
this.setStrokeWidth = elemGetSet.setStrokeWidthMethod; // Sets the stroke width for the current selected elements.
|
|
this.getResolution = elemGetSet.getResolutionMethod; // The current dimensions and zoom level in an object
|
|
this.getTitle = elemGetSet.getTitleMethod; // the current group/SVG's title contents or `undefined` if no element
|
|
this.setGroupTitle = elemGetSet.setGroupTitleMethod; // Sets the group/SVG's title content.
|
|
this.setStrokeAttr = elemGetSet.setStrokeAttrMethod; // Set the given stroke-related attribute the given value for selected elements.
|
|
this.setBackground = elemGetSet.setBackgroundMethod; // Set the background of the editor (NOT the actual document).
|
|
this.setDocumentTitle = elemGetSet.setDocumentTitleMethod; // Adds/updates a title element for the document with the given name.
|
|
this.getEditorNS = elemGetSet.getEditorNSMethod; // Returns the editor's namespace URL, optionally adding it to the root element.
|
|
this.setResolution = elemGetSet.setResolutionMethod; // Changes the document's dimensions to the given size.
|
|
this.setBBoxZoom = elemGetSet.setBBoxZoomMethod; // Sets the zoom level on the canvas-side based on the given value.
|
|
this.setCurrentZoom = elemGetSet.setZoomMethod; // Sets the zoom to the given level.
|
|
this.setColor = elemGetSet.setColorMethod; // Change the current stroke/fill color/gradien
|
|
this.setGradient = elemGetSet.setGradientMethod; // Apply the current gradient to selected element's fill or stroke.
|
|
this.setPaint = elemGetSet.setPaintMethod; // Set a color/gradient to a fill/stroke.
|
|
this.changeSelectedAttributeNoUndo = changeSelectedAttributeNoUndoMethod; // This function makes the changes to the elements. It does not add the change to the history stack.
|
|
this.changeSelectedAttribute = changeSelectedAttributeMethod; // Change the given/selected element and add the original value to the history stack.
|
|
this.deleteSelectedElements = deleteSelectedElements; // Removes all selected elements from the DOM and adds the change to the history
|
|
this.setBlurNoUndo = setBlurNoUndo; // Sets the `stdDeviation` blur value on the selected element without being undoable.
|
|
this.setBlurOffsets = setBlurOffsets; // Sets the `x`, `y`, `width`, `height` values of the filter element in order to make the blur not be clipped. Removes them if not neeeded.
|
|
this.setBlur = setBlur; // Adds/updates the blur filter to the selected element.
|
|
this.smoothControlPoints = pathModule.smoothControlPoints;
|
|
this.getTypeMap = getTypeMap;
|
|
this.history = history; // object with all histor methods
|
|
this.NS = NS;
|
|
this.$id = $id;
|
|
this.$qq = $qq;
|
|
this.$qa = $qa;
|
|
}
|
|
} // End class
|
|
|
|
// attach utilities function to the class that are used by SvgEdit so
|
|
// we can avoid using the whole utilities.js file in svgEdit.js
|
|
SvgCanvas.$id = $id;
|
|
SvgCanvas.$qq = $qq;
|
|
SvgCanvas.$qa = $qa;
|
|
SvgCanvas.isNullish = isNullish;
|
|
SvgCanvas.encode64 = encode64;
|
|
SvgCanvas.decode64 = decode64;
|
|
SvgCanvas.mergeDeep = mergeDeep;
|
|
SvgCanvas.getClosest = getClosest;
|
|
SvgCanvas.getParents = getParents;
|
|
SvgCanvas.blankPageObjectURL = blankPageObjectURL;
|
|
|
|
export default SvgCanvas;
|