{"version":3,"file":"index-umd.min.js","sources":["../editor/touch.js","../editor/svgedit.js","../editor/pathseg.js","../editor/browser.js","../editor/canvg/rgbcolor.js","../editor/jquery-svg.js","../editor/external/dynamic-import-polyfill/importModule.js","../editor/svgtransformlist.js","../editor/units.js","../editor/history.js","../editor/math.js","../editor/path.js","../editor/svgutils.js","../editor/contextmenu.js","../editor/canvg/canvg.js","../editor/layer.js","../editor/historyrecording.js","../editor/draw.js","../editor/sanitize.js","../editor/coords.js","../editor/recalculate.js","../editor/select.js","../editor/svgcanvas.js","../editor/jgraduate/jquery.jgraduate.js","../editor/jgraduate/jpicker.js","../editor/locale/locale.js","../editor/svg-editor.js","../editor/js-hotkeys/jquery.hotkeys.min.js","../editor/jquerybbq/jquery.bbq.min.js","../editor/svgicons/jquery.svgicons.js","../editor/spinbtn/JQuerySpinBtn.js","../editor/contextmenu/jquery.contextMenu.js","../editor/external/load-stylesheets/index-es.js"],"sourcesContent":["// http://ross.posterous.com/2008/08/19/iphone-touch-events-in-javascript/\nfunction touchHandler (event) {\n const touches = event.changedTouches,\n first = touches[0];\n let type = '';\n switch (event.type) {\n case 'touchstart': type = 'mousedown'; break;\n case 'touchmove': type = 'mousemove'; break;\n case 'touchend': type = 'mouseup'; break;\n default: return;\n }\n\n // initMouseEvent(type, canBubble, cancelable, view, clickCount,\n // screenX, screenY, clientX, clientY, ctrlKey,\n // altKey, shiftKey, metaKey, button, relatedTarget);\n\n const simulatedEvent = document.createEvent('MouseEvent');\n simulatedEvent.initMouseEvent(type, true, true, window, 1,\n first.screenX, first.screenY,\n first.clientX, first.clientY, false,\n false, false, false, 0/* left */, null);\n if (touches.length < 2) {\n first.target.dispatchEvent(simulatedEvent);\n event.preventDefault();\n }\n}\n\ndocument.addEventListener('touchstart', touchHandler, true);\ndocument.addEventListener('touchmove', touchHandler, true);\ndocument.addEventListener('touchend', touchHandler, true);\ndocument.addEventListener('touchcancel', touchHandler, true);\n","/**\n *\n * Licensed under the MIT License\n */\n\n/**\n* Common namepaces constants in alpha order\n*/\nexport const NS = {\n HTML: 'http://www.w3.org/1999/xhtml',\n MATH: 'http://www.w3.org/1998/Math/MathML',\n SE: 'http://svg-edit.googlecode.com',\n SVG: 'http://www.w3.org/2000/svg',\n XLINK: 'http://www.w3.org/1999/xlink',\n XML: 'http://www.w3.org/XML/1998/namespace',\n XMLNS: 'http://www.w3.org/2000/xmlns/' // see http://www.w3.org/TR/REC-xml-names/#xmlReserved\n};\n\n/**\n* @returns The NS with key values switched and lowercase\n*/\nexport const getReverseNS = function () {\n const reverseNS = {};\n Object.entries(NS).forEach(([name, URI]) => {\n reverseNS[URI] = name.toLowerCase();\n });\n return reverseNS;\n};\n","// SVGPathSeg API polyfill\n// https://github.com/progers/pathseg\n//\n// This is a drop-in replacement for the SVGPathSeg and SVGPathSegList APIs that were removed from\n// SVG2 (https://lists.w3.org/Archives/Public/www-svg/2015Jun/0044.html), including the latest spec\n// changes which were implemented in Firefox 43 and Chrome 46.\n\n(() => {\nif (!('SVGPathSeg' in window)) {\n // Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSeg\n class SVGPathSeg {\n constructor (type, typeAsLetter, owningPathSegList) {\n this.pathSegType = type;\n this.pathSegTypeAsLetter = typeAsLetter;\n this._owningPathSegList = owningPathSegList;\n }\n // Notify owning PathSegList on any changes so they can be synchronized back to the path element.\n _segmentChanged () {\n if (this._owningPathSegList) {\n this._owningPathSegList.segmentChanged(this);\n }\n }\n }\n SVGPathSeg.prototype.classname = 'SVGPathSeg';\n\n SVGPathSeg.PATHSEG_UNKNOWN = 0;\n SVGPathSeg.PATHSEG_CLOSEPATH = 1;\n SVGPathSeg.PATHSEG_MOVETO_ABS = 2;\n SVGPathSeg.PATHSEG_MOVETO_REL = 3;\n SVGPathSeg.PATHSEG_LINETO_ABS = 4;\n SVGPathSeg.PATHSEG_LINETO_REL = 5;\n SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS = 6;\n SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL = 7;\n SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS = 8;\n SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL = 9;\n SVGPathSeg.PATHSEG_ARC_ABS = 10;\n SVGPathSeg.PATHSEG_ARC_REL = 11;\n SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS = 12;\n SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL = 13;\n SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS = 14;\n SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL = 15;\n SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS = 16;\n SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL = 17;\n SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS = 18;\n SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL = 19;\n\n class SVGPathSegClosePath extends SVGPathSeg {\n constructor (owningPathSegList) {\n super(SVGPathSeg.PATHSEG_CLOSEPATH, 'z', owningPathSegList);\n }\n toString () { return '[object SVGPathSegClosePath]'; }\n _asPathString () { return this.pathSegTypeAsLetter; }\n clone () { return new SVGPathSegClosePath(undefined); }\n }\n\n class SVGPathSegMovetoAbs extends SVGPathSeg {\n constructor (owningPathSegList, x, y) {\n super(SVGPathSeg.PATHSEG_MOVETO_ABS, 'M', owningPathSegList);\n this._x = x;\n this._y = y;\n }\n toString () { return '[object SVGPathSegMovetoAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegMovetoAbs(undefined, this._x, this._y); }\n }\n Object.defineProperties(SVGPathSegMovetoAbs.prototype, {\n x: {\n get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true\n },\n y: {\n get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true\n }\n });\n\n class SVGPathSegMovetoRel extends SVGPathSeg {\n constructor (owningPathSegList, x, y) {\n super(SVGPathSeg.PATHSEG_MOVETO_REL, 'm', owningPathSegList);\n this._x = x;\n this._y = y;\n }\n toString () { return '[object SVGPathSegMovetoRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegMovetoRel(undefined, this._x, this._y); }\n }\n Object.defineProperties(SVGPathSegMovetoRel.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegLinetoAbs extends SVGPathSeg {\n constructor (owningPathSegList, x, y) {\n super(SVGPathSeg.PATHSEG_LINETO_ABS, 'L', owningPathSegList);\n this._x = x;\n this._y = y;\n }\n toString () { return '[object SVGPathSegLinetoAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegLinetoAbs(undefined, this._x, this._y); }\n }\n Object.defineProperties(SVGPathSegLinetoAbs.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegLinetoRel extends SVGPathSeg {\n constructor (owningPathSegList, x, y) {\n super(SVGPathSeg.PATHSEG_LINETO_REL, 'l', owningPathSegList);\n this._x = x;\n this._y = y;\n }\n toString () { return '[object SVGPathSegLinetoRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegLinetoRel(undefined, this._x, this._y); }\n }\n Object.defineProperties(SVGPathSegLinetoRel.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegCurvetoCubicAbs extends SVGPathSeg {\n constructor (owningPathSegList, x, y, x1, y1, x2, y2) {\n super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS, 'C', owningPathSegList);\n this._x = x;\n this._y = y;\n this._x1 = x1;\n this._y1 = y1;\n this._x2 = x2;\n this._y2 = y2;\n }\n toString () { return '[object SVGPathSegCurvetoCubicAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoCubicAbs(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }\n }\n Object.defineProperties(SVGPathSegCurvetoCubicAbs.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n x1: { get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true },\n y1: { get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true },\n x2: { get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true },\n y2: { get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegCurvetoCubicRel extends SVGPathSeg {\n constructor (owningPathSegList, x, y, x1, y1, x2, y2) {\n super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL, 'c', owningPathSegList);\n this._x = x;\n this._y = y;\n this._x1 = x1;\n this._y1 = y1;\n this._x2 = x2;\n this._y2 = y2;\n }\n toString () { return '[object SVGPathSegCurvetoCubicRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoCubicRel(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }\n }\n Object.defineProperties(SVGPathSegCurvetoCubicRel.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n x1: { get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true },\n y1: { get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true },\n x2: { get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true },\n y2: { get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegCurvetoQuadraticAbs extends SVGPathSeg {\n constructor (owningPathSegList, x, y, x1, y1) {\n super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS, 'Q', owningPathSegList);\n this._x = x;\n this._y = y;\n this._x1 = x1;\n this._y1 = y1;\n }\n toString () { return '[object SVGPathSegCurvetoQuadraticAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoQuadraticAbs(undefined, this._x, this._y, this._x1, this._y1); }\n }\n Object.defineProperties(SVGPathSegCurvetoQuadraticAbs.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n x1: { get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true },\n y1: { get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegCurvetoQuadraticRel extends SVGPathSeg {\n constructor (owningPathSegList, x, y, x1, y1) {\n super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL, 'q', owningPathSegList);\n this._x = x;\n this._y = y;\n this._x1 = x1;\n this._y1 = y1;\n }\n toString () { return '[object SVGPathSegCurvetoQuadraticRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoQuadraticRel(undefined, this._x, this._y, this._x1, this._y1); }\n }\n Object.defineProperties(SVGPathSegCurvetoQuadraticRel.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n x1: { get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true },\n y1: { get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegArcAbs extends SVGPathSeg {\n constructor (owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {\n super(SVGPathSeg.PATHSEG_ARC_ABS, 'A', owningPathSegList);\n this._x = x;\n this._y = y;\n this._r1 = r1;\n this._r2 = r2;\n this._angle = angle;\n this._largeArcFlag = largeArcFlag;\n this._sweepFlag = sweepFlag;\n }\n toString () { return '[object SVGPathSegArcAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegArcAbs(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }\n }\n Object.defineProperties(SVGPathSegArcAbs.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n r1: { get () { return this._r1; }, set (r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true },\n r2: { get () { return this._r2; }, set (r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true },\n angle: { get () { return this._angle; }, set (angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true },\n largeArcFlag: { get () { return this._largeArcFlag; }, set (largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true },\n sweepFlag: { get () { return this._sweepFlag; }, set (sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegArcRel extends SVGPathSeg {\n constructor (owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {\n super(SVGPathSeg.PATHSEG_ARC_REL, 'a', owningPathSegList);\n this._x = x;\n this._y = y;\n this._r1 = r1;\n this._r2 = r2;\n this._angle = angle;\n this._largeArcFlag = largeArcFlag;\n this._sweepFlag = sweepFlag;\n }\n toString () { return '[object SVGPathSegArcRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegArcRel(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }\n }\n Object.defineProperties(SVGPathSegArcRel.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n r1: { get () { return this._r1; }, set (r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true },\n r2: { get () { return this._r2; }, set (r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true },\n angle: { get () { return this._angle; }, set (angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true },\n largeArcFlag: { get () { return this._largeArcFlag; }, set (largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true },\n sweepFlag: { get () { return this._sweepFlag; }, set (sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegLinetoHorizontalAbs extends SVGPathSeg {\n constructor (owningPathSegList, x) {\n super(SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS, 'H', owningPathSegList);\n this._x = x;\n }\n toString () { return '[object SVGPathSegLinetoHorizontalAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x; }\n clone () { return new SVGPathSegLinetoHorizontalAbs(undefined, this._x); }\n }\n Object.defineProperty(SVGPathSegLinetoHorizontalAbs.prototype, 'x', { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true });\n\n class SVGPathSegLinetoHorizontalRel extends SVGPathSeg {\n constructor (owningPathSegList, x) {\n super(SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL, 'h', owningPathSegList);\n this._x = x;\n }\n toString () { return '[object SVGPathSegLinetoHorizontalRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x; }\n clone () { return new SVGPathSegLinetoHorizontalRel(undefined, this._x); }\n }\n Object.defineProperty(SVGPathSegLinetoHorizontalRel.prototype, 'x', { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true });\n\n class SVGPathSegLinetoVerticalAbs extends SVGPathSeg {\n constructor (owningPathSegList, y) {\n super(SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS, 'V', owningPathSegList);\n this._y = y;\n }\n toString () { return '[object SVGPathSegLinetoVerticalAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._y; }\n clone () { return new SVGPathSegLinetoVerticalAbs(undefined, this._y); }\n }\n Object.defineProperty(SVGPathSegLinetoVerticalAbs.prototype, 'y', { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true });\n\n class SVGPathSegLinetoVerticalRel extends SVGPathSeg {\n constructor (owningPathSegList, y) {\n super(SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL, 'v', owningPathSegList);\n this._y = y;\n }\n toString () { return '[object SVGPathSegLinetoVerticalRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._y; }\n clone () { return new SVGPathSegLinetoVerticalRel(undefined, this._y); }\n }\n Object.defineProperty(SVGPathSegLinetoVerticalRel.prototype, 'y', { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true });\n\n class SVGPathSegCurvetoCubicSmoothAbs extends SVGPathSeg {\n constructor (owningPathSegList, x, y, x2, y2) {\n super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS, 'S', owningPathSegList);\n this._x = x;\n this._y = y;\n this._x2 = x2;\n this._y2 = y2;\n }\n toString () { return '[object SVGPathSegCurvetoCubicSmoothAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoCubicSmoothAbs(undefined, this._x, this._y, this._x2, this._y2); }\n }\n Object.defineProperties(SVGPathSegCurvetoCubicSmoothAbs.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n x2: { get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true },\n y2: { get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegCurvetoCubicSmoothRel extends SVGPathSeg {\n constructor (owningPathSegList, x, y, x2, y2) {\n super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL, 's', owningPathSegList);\n this._x = x;\n this._y = y;\n this._x2 = x2;\n this._y2 = y2;\n }\n toString () { return '[object SVGPathSegCurvetoCubicSmoothRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoCubicSmoothRel(undefined, this._x, this._y, this._x2, this._y2); }\n }\n Object.defineProperties(SVGPathSegCurvetoCubicSmoothRel.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true },\n x2: { get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true },\n y2: { get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegCurvetoQuadraticSmoothAbs extends SVGPathSeg {\n constructor (owningPathSegList, x, y) {\n super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS, 'T', owningPathSegList);\n this._x = x;\n this._y = y;\n }\n toString () { return '[object SVGPathSegCurvetoQuadraticSmoothAbs]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoQuadraticSmoothAbs(undefined, this._x, this._y); }\n }\n Object.defineProperties(SVGPathSegCurvetoQuadraticSmoothAbs.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true }\n });\n\n class SVGPathSegCurvetoQuadraticSmoothRel extends SVGPathSeg {\n constructor (owningPathSegList, x, y) {\n super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL, 't', owningPathSegList);\n this._x = x;\n this._y = y;\n }\n toString () { return '[object SVGPathSegCurvetoQuadraticSmoothRel]'; }\n _asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }\n clone () { return new SVGPathSegCurvetoQuadraticSmoothRel(undefined, this._x, this._y); }\n }\n Object.defineProperties(SVGPathSegCurvetoQuadraticSmoothRel.prototype, {\n x: { get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true },\n y: { get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true }\n });\n\n // Add createSVGPathSeg* functions to SVGPathElement.\n // Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathElement.\n SVGPathElement.prototype.createSVGPathSegClosePath = function () { return new SVGPathSegClosePath(undefined); };\n SVGPathElement.prototype.createSVGPathSegMovetoAbs = function (x, y) { return new SVGPathSegMovetoAbs(undefined, x, y); };\n SVGPathElement.prototype.createSVGPathSegMovetoRel = function (x, y) { return new SVGPathSegMovetoRel(undefined, x, y); };\n SVGPathElement.prototype.createSVGPathSegLinetoAbs = function (x, y) { return new SVGPathSegLinetoAbs(undefined, x, y); };\n SVGPathElement.prototype.createSVGPathSegLinetoRel = function (x, y) { return new SVGPathSegLinetoRel(undefined, x, y); };\n SVGPathElement.prototype.createSVGPathSegCurvetoCubicAbs = function (x, y, x1, y1, x2, y2) { return new SVGPathSegCurvetoCubicAbs(undefined, x, y, x1, y1, x2, y2); };\n SVGPathElement.prototype.createSVGPathSegCurvetoCubicRel = function (x, y, x1, y1, x2, y2) { return new SVGPathSegCurvetoCubicRel(undefined, x, y, x1, y1, x2, y2); };\n SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticAbs = function (x, y, x1, y1) { return new SVGPathSegCurvetoQuadraticAbs(undefined, x, y, x1, y1); };\n SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticRel = function (x, y, x1, y1) { return new SVGPathSegCurvetoQuadraticRel(undefined, x, y, x1, y1); };\n SVGPathElement.prototype.createSVGPathSegArcAbs = function (x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new SVGPathSegArcAbs(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); };\n SVGPathElement.prototype.createSVGPathSegArcRel = function (x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new SVGPathSegArcRel(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); };\n SVGPathElement.prototype.createSVGPathSegLinetoHorizontalAbs = function (x) { return new SVGPathSegLinetoHorizontalAbs(undefined, x); };\n SVGPathElement.prototype.createSVGPathSegLinetoHorizontalRel = function (x) { return new SVGPathSegLinetoHorizontalRel(undefined, x); };\n SVGPathElement.prototype.createSVGPathSegLinetoVerticalAbs = function (y) { return new SVGPathSegLinetoVerticalAbs(undefined, y); };\n SVGPathElement.prototype.createSVGPathSegLinetoVerticalRel = function (y) { return new SVGPathSegLinetoVerticalRel(undefined, y); };\n SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothAbs = function (x, y, x2, y2) { return new SVGPathSegCurvetoCubicSmoothAbs(undefined, x, y, x2, y2); };\n SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothRel = function (x, y, x2, y2) { return new SVGPathSegCurvetoCubicSmoothRel(undefined, x, y, x2, y2); };\n SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothAbs = function (x, y) { return new SVGPathSegCurvetoQuadraticSmoothAbs(undefined, x, y); };\n SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothRel = function (x, y) { return new SVGPathSegCurvetoQuadraticSmoothRel(undefined, x, y); };\n\n if (!('getPathSegAtLength' in SVGPathElement.prototype)) {\n // Add getPathSegAtLength to SVGPathElement.\n // Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-__svg__SVGPathElement__getPathSegAtLength\n // This polyfill requires SVGPathElement.getTotalLength to implement the distance-along-a-path algorithm.\n SVGPathElement.prototype.getPathSegAtLength = function (distance) {\n if (distance === undefined || !isFinite(distance)) {\n throw new Error('Invalid arguments.');\n }\n\n const measurementElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n measurementElement.setAttribute('d', this.getAttribute('d'));\n let lastPathSegment = measurementElement.pathSegList.numberOfItems - 1;\n\n // If the path is empty, return 0.\n if (lastPathSegment <= 0) {\n return 0;\n }\n\n do {\n measurementElement.pathSegList.removeItem(lastPathSegment);\n if (distance > measurementElement.getTotalLength()) {\n break;\n }\n lastPathSegment--;\n } while (lastPathSegment > 0);\n return lastPathSegment;\n };\n }\n\n window.SVGPathSeg = SVGPathSeg;\n window.SVGPathSegClosePath = SVGPathSegClosePath;\n window.SVGPathSegMovetoAbs = SVGPathSegMovetoAbs;\n window.SVGPathSegMovetoRel = SVGPathSegMovetoRel;\n window.SVGPathSegLinetoAbs = SVGPathSegLinetoAbs;\n window.SVGPathSegLinetoRel = SVGPathSegLinetoRel;\n window.SVGPathSegCurvetoCubicAbs = SVGPathSegCurvetoCubicAbs;\n window.SVGPathSegCurvetoCubicRel = SVGPathSegCurvetoCubicRel;\n window.SVGPathSegCurvetoQuadraticAbs = SVGPathSegCurvetoQuadraticAbs;\n window.SVGPathSegCurvetoQuadraticRel = SVGPathSegCurvetoQuadraticRel;\n window.SVGPathSegArcAbs = SVGPathSegArcAbs;\n window.SVGPathSegArcRel = SVGPathSegArcRel;\n window.SVGPathSegLinetoHorizontalAbs = SVGPathSegLinetoHorizontalAbs;\n window.SVGPathSegLinetoHorizontalRel = SVGPathSegLinetoHorizontalRel;\n window.SVGPathSegLinetoVerticalAbs = SVGPathSegLinetoVerticalAbs;\n window.SVGPathSegLinetoVerticalRel = SVGPathSegLinetoVerticalRel;\n window.SVGPathSegCurvetoCubicSmoothAbs = SVGPathSegCurvetoCubicSmoothAbs;\n window.SVGPathSegCurvetoCubicSmoothRel = SVGPathSegCurvetoCubicSmoothRel;\n window.SVGPathSegCurvetoQuadraticSmoothAbs = SVGPathSegCurvetoQuadraticSmoothAbs;\n window.SVGPathSegCurvetoQuadraticSmoothRel = SVGPathSegCurvetoQuadraticSmoothRel;\n}\n\n// Checking for SVGPathSegList in window checks for the case of an implementation without the\n// SVGPathSegList API.\n// The second check for appendItem is specific to Firefox 59+ which removed only parts of the\n// SVGPathSegList API (e.g., appendItem). In this case we need to re-implement the entire API\n// so the polyfill data (i.e., _list) is used throughout.\nif (!('SVGPathSegList' in window) || !('appendItem' in SVGPathSegList.prototype)) {\n // Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSegList\n class SVGPathSegList {\n constructor (pathElement) {\n this._pathElement = pathElement;\n this._list = this._parsePath(this._pathElement.getAttribute('d'));\n\n // Use a MutationObserver to catch changes to the path's \"d\" attribute.\n this._mutationObserverConfig = {attributes: true, attributeFilter: ['d']};\n this._pathElementMutationObserver = new MutationObserver(this._updateListFromPathMutations.bind(this));\n this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);\n }\n // Process any pending mutations to the path element and update the list as needed.\n // This should be the first call of all public functions and is needed because\n // MutationObservers are not synchronous so we can have pending asynchronous mutations.\n _checkPathSynchronizedToList () {\n this._updateListFromPathMutations(this._pathElementMutationObserver.takeRecords());\n }\n\n _updateListFromPathMutations (mutationRecords) {\n if (!this._pathElement) {\n return;\n }\n let hasPathMutations = false;\n mutationRecords.forEach(function (record) {\n if (record.attributeName === 'd') {\n hasPathMutations = true;\n }\n });\n if (hasPathMutations) {\n this._list = this._parsePath(this._pathElement.getAttribute('d'));\n }\n }\n\n // Serialize the list and update the path's 'd' attribute.\n _writeListToPath () {\n this._pathElementMutationObserver.disconnect();\n this._pathElement.setAttribute('d', SVGPathSegList._pathSegArrayAsString(this._list));\n this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);\n }\n\n // When a path segment changes the list needs to be synchronized back to the path element.\n segmentChanged (pathSeg) {\n this._writeListToPath();\n }\n\n clear () {\n this._checkPathSynchronizedToList();\n\n this._list.forEach(function (pathSeg) {\n pathSeg._owningPathSegList = null;\n });\n this._list = [];\n this._writeListToPath();\n }\n\n initialize (newItem) {\n this._checkPathSynchronizedToList();\n\n this._list = [newItem];\n newItem._owningPathSegList = this;\n this._writeListToPath();\n return newItem;\n }\n\n _checkValidIndex (index) {\n if (isNaN(index) || index < 0 || index >= this.numberOfItems) {\n throw new Error('INDEX_SIZE_ERR');\n }\n }\n\n getItem (index) {\n this._checkPathSynchronizedToList();\n\n this._checkValidIndex(index);\n return this._list[index];\n }\n\n insertItemBefore (newItem, index) {\n this._checkPathSynchronizedToList();\n\n // Spec: If the index is greater than or equal to numberOfItems, then the new item is appended to the end of the list.\n if (index > this.numberOfItems) {\n index = this.numberOfItems;\n }\n if (newItem._owningPathSegList) {\n // SVG2 spec says to make a copy.\n newItem = newItem.clone();\n }\n this._list.splice(index, 0, newItem);\n newItem._owningPathSegList = this;\n this._writeListToPath();\n return newItem;\n }\n\n replaceItem (newItem, index) {\n this._checkPathSynchronizedToList();\n\n if (newItem._owningPathSegList) {\n // SVG2 spec says to make a copy.\n newItem = newItem.clone();\n }\n this._checkValidIndex(index);\n this._list[index] = newItem;\n newItem._owningPathSegList = this;\n this._writeListToPath();\n return newItem;\n }\n\n removeItem (index) {\n this._checkPathSynchronizedToList();\n\n this._checkValidIndex(index);\n const item = this._list[index];\n this._list.splice(index, 1);\n this._writeListToPath();\n return item;\n }\n\n appendItem (newItem) {\n this._checkPathSynchronizedToList();\n\n if (newItem._owningPathSegList) {\n // SVG2 spec says to make a copy.\n newItem = newItem.clone();\n }\n this._list.push(newItem);\n newItem._owningPathSegList = this;\n // TODO: Optimize this to just append to the existing attribute.\n this._writeListToPath();\n return newItem;\n }\n\n // This closely follows SVGPathParser::parsePath from Source/core/svg/SVGPathParser.cpp.\n _parsePath (string) {\n if (!string || !string.length) {\n return [];\n }\n\n const owningPathSegList = this;\n\n class Builder {\n constructor () {\n this.pathSegList = [];\n }\n appendSegment (pathSeg) {\n this.pathSegList.push(pathSeg);\n }\n }\n\n class Source {\n constructor (string) {\n this._string = string;\n this._currentIndex = 0;\n this._endIndex = this._string.length;\n this._previousCommand = SVGPathSeg.PATHSEG_UNKNOWN;\n\n this._skipOptionalSpaces();\n }\n _isCurrentSpace () {\n const character = this._string[this._currentIndex];\n return character <= ' ' && (character === ' ' || character === '\\n' || character === '\\t' || character === '\\r' || character === '\\f');\n }\n\n _skipOptionalSpaces () {\n while (this._currentIndex < this._endIndex && this._isCurrentSpace()) {\n this._currentIndex++;\n }\n return this._currentIndex < this._endIndex;\n }\n\n _skipOptionalSpacesOrDelimiter () {\n if (this._currentIndex < this._endIndex && !this._isCurrentSpace() && this._string.charAt(this._currentIndex) !== ',') {\n return false;\n }\n if (this._skipOptionalSpaces()) {\n if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === ',') {\n this._currentIndex++;\n this._skipOptionalSpaces();\n }\n }\n return this._currentIndex < this._endIndex;\n }\n\n hasMoreData () {\n return this._currentIndex < this._endIndex;\n }\n\n peekSegmentType () {\n const lookahead = this._string[this._currentIndex];\n return this._pathSegTypeFromChar(lookahead);\n }\n\n _pathSegTypeFromChar (lookahead) {\n switch (lookahead) {\n case 'Z':\n case 'z':\n return SVGPathSeg.PATHSEG_CLOSEPATH;\n case 'M':\n return SVGPathSeg.PATHSEG_MOVETO_ABS;\n case 'm':\n return SVGPathSeg.PATHSEG_MOVETO_REL;\n case 'L':\n return SVGPathSeg.PATHSEG_LINETO_ABS;\n case 'l':\n return SVGPathSeg.PATHSEG_LINETO_REL;\n case 'C':\n return SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS;\n case 'c':\n return SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL;\n case 'Q':\n return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS;\n case 'q':\n return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL;\n case 'A':\n return SVGPathSeg.PATHSEG_ARC_ABS;\n case 'a':\n return SVGPathSeg.PATHSEG_ARC_REL;\n case 'H':\n return SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS;\n case 'h':\n return SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL;\n case 'V':\n return SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS;\n case 'v':\n return SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL;\n case 'S':\n return SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS;\n case 's':\n return SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL;\n case 'T':\n return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS;\n case 't':\n return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL;\n default:\n return SVGPathSeg.PATHSEG_UNKNOWN;\n }\n }\n\n _nextCommandHelper (lookahead, previousCommand) {\n // Check for remaining coordinates in the current command.\n if ((lookahead === '+' || lookahead === '-' || lookahead === '.' || (lookahead >= '0' && lookahead <= '9')) && previousCommand !== SVGPathSeg.PATHSEG_CLOSEPATH) {\n if (previousCommand === SVGPathSeg.PATHSEG_MOVETO_ABS) {\n return SVGPathSeg.PATHSEG_LINETO_ABS;\n }\n if (previousCommand === SVGPathSeg.PATHSEG_MOVETO_REL) {\n return SVGPathSeg.PATHSEG_LINETO_REL;\n }\n return previousCommand;\n }\n return SVGPathSeg.PATHSEG_UNKNOWN;\n }\n\n initialCommandIsMoveTo () {\n // If the path is empty it is still valid, so return true.\n if (!this.hasMoreData()) {\n return true;\n }\n const command = this.peekSegmentType();\n // Path must start with moveTo.\n return command === SVGPathSeg.PATHSEG_MOVETO_ABS || command === SVGPathSeg.PATHSEG_MOVETO_REL;\n }\n\n // Parse a number from an SVG path. This very closely follows genericParseNumber(...) from Source/core/svg/SVGParserUtilities.cpp.\n // Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-PathDataBNF\n _parseNumber () {\n let exponent = 0;\n let integer = 0;\n let frac = 1;\n let decimal = 0;\n let sign = 1;\n let expsign = 1;\n\n const startIndex = this._currentIndex;\n\n this._skipOptionalSpaces();\n\n // Read the sign.\n if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '+') {\n this._currentIndex++;\n } else if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '-') {\n this._currentIndex++;\n sign = -1;\n }\n\n if (this._currentIndex === this._endIndex || ((this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') && this._string.charAt(this._currentIndex) !== '.')) {\n // The first character of a number must be one of [0-9+-.].\n return undefined;\n }\n\n // Read the integer part, build right-to-left.\n const startIntPartIndex = this._currentIndex;\n while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {\n this._currentIndex++; // Advance to first non-digit.\n }\n\n if (this._currentIndex !== startIntPartIndex) {\n let scanIntPartIndex = this._currentIndex - 1;\n let multiplier = 1;\n while (scanIntPartIndex >= startIntPartIndex) {\n integer += multiplier * (this._string.charAt(scanIntPartIndex--) - '0');\n multiplier *= 10;\n }\n }\n\n // Read the decimals.\n if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '.') {\n this._currentIndex++;\n\n // There must be a least one digit following the .\n if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') {\n return undefined;\n }\n while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {\n frac *= 10;\n decimal += (this._string.charAt(this._currentIndex) - '0') / frac;\n this._currentIndex += 1;\n }\n }\n\n // Read the exponent part.\n if (this._currentIndex !== startIndex && this._currentIndex + 1 < this._endIndex && (this._string.charAt(this._currentIndex) === 'e' || this._string.charAt(this._currentIndex) === 'E') && (this._string.charAt(this._currentIndex + 1) !== 'x' && this._string.charAt(this._currentIndex + 1) !== 'm')) {\n this._currentIndex++;\n\n // Read the sign of the exponent.\n if (this._string.charAt(this._currentIndex) === '+') {\n this._currentIndex++;\n } else if (this._string.charAt(this._currentIndex) === '-') {\n this._currentIndex++;\n expsign = -1;\n }\n\n // There must be an exponent.\n if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') {\n return undefined;\n }\n\n while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {\n exponent *= 10;\n exponent += (this._string.charAt(this._currentIndex) - '0');\n this._currentIndex++;\n }\n }\n\n let number = integer + decimal;\n number *= sign;\n\n if (exponent) {\n number *= Math.pow(10, expsign * exponent);\n }\n\n if (startIndex === this._currentIndex) {\n return undefined;\n }\n\n this._skipOptionalSpacesOrDelimiter();\n\n return number;\n }\n\n _parseArcFlag () {\n if (this._currentIndex >= this._endIndex) {\n return undefined;\n }\n let flag = false;\n const flagChar = this._string.charAt(this._currentIndex++);\n if (flagChar === '0') {\n flag = false;\n } else if (flagChar === '1') {\n flag = true;\n } else {\n return undefined;\n }\n\n this._skipOptionalSpacesOrDelimiter();\n return flag;\n }\n\n parseSegment () {\n const lookahead = this._string[this._currentIndex];\n let command = this._pathSegTypeFromChar(lookahead);\n if (command === SVGPathSeg.PATHSEG_UNKNOWN) {\n // Possibly an implicit command. Not allowed if this is the first command.\n if (this._previousCommand === SVGPathSeg.PATHSEG_UNKNOWN) {\n return null;\n }\n command = this._nextCommandHelper(lookahead, this._previousCommand);\n if (command === SVGPathSeg.PATHSEG_UNKNOWN) {\n return null;\n }\n } else {\n this._currentIndex++;\n }\n\n this._previousCommand = command;\n\n switch (command) {\n case SVGPathSeg.PATHSEG_MOVETO_REL:\n return new SVGPathSegMovetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());\n case SVGPathSeg.PATHSEG_MOVETO_ABS:\n return new SVGPathSegMovetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());\n case SVGPathSeg.PATHSEG_LINETO_REL:\n return new SVGPathSegLinetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());\n case SVGPathSeg.PATHSEG_LINETO_ABS:\n return new SVGPathSegLinetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());\n case SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL:\n return new SVGPathSegLinetoHorizontalRel(owningPathSegList, this._parseNumber());\n case SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS:\n return new SVGPathSegLinetoHorizontalAbs(owningPathSegList, this._parseNumber());\n case SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL:\n return new SVGPathSegLinetoVerticalRel(owningPathSegList, this._parseNumber());\n case SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS:\n return new SVGPathSegLinetoVerticalAbs(owningPathSegList, this._parseNumber());\n case SVGPathSeg.PATHSEG_CLOSEPATH:\n this._skipOptionalSpaces();\n return new SVGPathSegClosePath(owningPathSegList);\n case SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL: {\n const points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegCurvetoCubicRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);\n } case SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS: {\n const points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegCurvetoCubicAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);\n } case SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL: {\n const points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegCurvetoCubicSmoothRel(owningPathSegList, points.x, points.y, points.x2, points.y2);\n } case SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS: {\n const points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegCurvetoCubicSmoothAbs(owningPathSegList, points.x, points.y, points.x2, points.y2);\n } case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL: {\n const points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegCurvetoQuadraticRel(owningPathSegList, points.x, points.y, points.x1, points.y1);\n } case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS:\n const points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegCurvetoQuadraticAbs(owningPathSegList, points.x, points.y, points.x1, points.y1);\n case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL:\n return new SVGPathSegCurvetoQuadraticSmoothRel(owningPathSegList, this._parseNumber(), this._parseNumber());\n case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS:\n return new SVGPathSegCurvetoQuadraticSmoothAbs(owningPathSegList, this._parseNumber(), this._parseNumber());\n case SVGPathSeg.PATHSEG_ARC_REL: {\n const points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegArcRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);\n } case SVGPathSeg.PATHSEG_ARC_ABS: {\n const points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};\n return new SVGPathSegArcAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);\n } default:\n throw new Error('Unknown path seg type.');\n }\n }\n }\n\n const builder = new Builder();\n const source = new Source(string);\n\n if (!source.initialCommandIsMoveTo()) {\n return [];\n }\n while (source.hasMoreData()) {\n const pathSeg = source.parseSegment();\n if (!pathSeg) {\n return [];\n }\n builder.appendSegment(pathSeg);\n }\n\n return builder.pathSegList;\n }\n }\n\n SVGPathSegList.prototype.classname = 'SVGPathSegList';\n\n Object.defineProperty(SVGPathSegList.prototype, 'numberOfItems', {\n get () {\n this._checkPathSynchronizedToList();\n return this._list.length;\n },\n enumerable: true\n });\n\n SVGPathSegList._pathSegArrayAsString = function (pathSegArray) {\n let string = '';\n let first = true;\n pathSegArray.forEach(function (pathSeg) {\n if (first) {\n first = false;\n string += pathSeg._asPathString();\n } else {\n string += ' ' + pathSeg._asPathString();\n }\n });\n return string;\n };\n\n // Add the pathSegList accessors to SVGPathElement.\n // Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGAnimatedPathData\n Object.defineProperties(SVGPathElement.prototype, {\n pathSegList: {\n get () {\n if (!this._pathSegList) {\n this._pathSegList = new SVGPathSegList(this);\n }\n return this._pathSegList;\n },\n enumerable: true\n },\n // FIXME: The following are not implemented and simply return SVGPathElement.pathSegList.\n normalizedPathSegList: { get () { return this.pathSegList; }, enumerable: true },\n animatedPathSegList: { get () { return this.pathSegList; }, enumerable: true },\n animatedNormalizedPathSegList: { get () { return this.pathSegList; }, enumerable: true }\n });\n window.SVGPathSegList = SVGPathSegList;\n}\n})();\n","/* globals jQuery */\n/**\n * Package: svgedit.browser\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Jeff Schiller\n * Copyright(c) 2010 Alexis Deveria\n */\n\n// Dependencies:\n// 1) jQuery (for $.alert())\n\nimport './pathseg.js';\nimport {NS} from './svgedit.js';\n\nconst $ = jQuery;\n\nconst supportsSvg_ = (function () {\nreturn !!document.createElementNS && !!document.createElementNS(NS.SVG, 'svg').createSVGRect;\n}());\n\nexport const supportsSvg = () => supportsSvg_;\n\nconst {userAgent} = navigator;\nconst svg = document.createElementNS(NS.SVG, 'svg');\n\n// Note: Browser sniffing should only be used if no other detection method is possible\nconst isOpera_ = !!window.opera;\nconst isWebkit_ = userAgent.includes('AppleWebKit');\nconst isGecko_ = userAgent.includes('Gecko/');\nconst isIE_ = userAgent.includes('MSIE');\nconst isChrome_ = userAgent.includes('Chrome/');\nconst isWindows_ = userAgent.includes('Windows');\nconst isMac_ = userAgent.includes('Macintosh');\nconst isTouch_ = 'ontouchstart' in window;\n\nconst supportsSelectors_ = (function () {\nreturn !!svg.querySelector;\n}());\n\nconst supportsXpath_ = (function () {\nreturn !!document.evaluate;\n}());\n\n// segList functions (for FF1.5 and 2.0)\nconst supportsPathReplaceItem_ = (function () {\nconst path = document.createElementNS(NS.SVG, 'path');\npath.setAttribute('d', 'M0,0 10,10');\nconst seglist = path.pathSegList;\nconst seg = path.createSVGPathSegLinetoAbs(5, 5);\ntry {\n seglist.replaceItem(seg, 1);\n return true;\n} catch (err) {}\nreturn false;\n}());\n\nconst supportsPathInsertItemBefore_ = (function () {\nconst path = document.createElementNS(NS.SVG, 'path');\npath.setAttribute('d', 'M0,0 10,10');\nconst seglist = path.pathSegList;\nconst seg = path.createSVGPathSegLinetoAbs(5, 5);\ntry {\n seglist.insertItemBefore(seg, 1);\n return true;\n} catch (err) {}\nreturn false;\n}());\n\n// text character positioning (for IE9)\nconst supportsGoodTextCharPos_ = (function () {\nconst svgroot = document.createElementNS(NS.SVG, 'svg');\nconst svgcontent = document.createElementNS(NS.SVG, 'svg');\ndocument.documentElement.appendChild(svgroot);\nsvgcontent.setAttribute('x', 5);\nsvgroot.appendChild(svgcontent);\nconst text = document.createElementNS(NS.SVG, 'text');\ntext.textContent = 'a';\nsvgcontent.appendChild(text);\nconst pos = text.getStartPositionOfChar(0).x;\ndocument.documentElement.removeChild(svgroot);\nreturn (pos === 0);\n}());\n\nconst supportsPathBBox_ = (function () {\nconst svgcontent = document.createElementNS(NS.SVG, 'svg');\ndocument.documentElement.appendChild(svgcontent);\nconst path = document.createElementNS(NS.SVG, 'path');\npath.setAttribute('d', 'M0,0 C0,0 10,10 10,0');\nsvgcontent.appendChild(path);\nconst bbox = path.getBBox();\ndocument.documentElement.removeChild(svgcontent);\nreturn (bbox.height > 4 && bbox.height < 5);\n}());\n\n// Support for correct bbox sizing on groups with horizontal/vertical lines\nconst supportsHVLineContainerBBox_ = (function () {\nconst svgcontent = document.createElementNS(NS.SVG, 'svg');\ndocument.documentElement.appendChild(svgcontent);\nconst path = document.createElementNS(NS.SVG, 'path');\npath.setAttribute('d', 'M0,0 10,0');\nconst path2 = document.createElementNS(NS.SVG, 'path');\npath2.setAttribute('d', 'M5,0 15,0');\nconst g = document.createElementNS(NS.SVG, 'g');\ng.appendChild(path);\ng.appendChild(path2);\nsvgcontent.appendChild(g);\nconst bbox = g.getBBox();\ndocument.documentElement.removeChild(svgcontent);\n// Webkit gives 0, FF gives 10, Opera (correctly) gives 15\nreturn (bbox.width === 15);\n}());\n\nconst supportsEditableText_ = (function () {\n// TODO: Find better way to check support for this\nreturn isOpera_;\n}());\n\nconst supportsGoodDecimals_ = (function () {\n// Correct decimals on clone attributes (Opera < 10.5/win/non-en)\nconst rect = document.createElementNS(NS.SVG, 'rect');\nrect.setAttribute('x', 0.1);\nconst crect = rect.cloneNode(false);\nconst retValue = (!crect.getAttribute('x').includes(','));\nif (!retValue) {\n // Todo: i18nize or remove\n $.alert('NOTE: This version of Opera is known to contain bugs in SVG-edit.\\n' +\n\t'Please upgrade to the latest version in which the problems have been fixed.');\n}\nreturn retValue;\n}());\n\nconst supportsNonScalingStroke_ = (function () {\nconst rect = document.createElementNS(NS.SVG, 'rect');\nrect.setAttribute('style', 'vector-effect:non-scaling-stroke');\nreturn rect.style.vectorEffect === 'non-scaling-stroke';\n}());\n\nlet supportsNativeSVGTransformLists_ = (function () {\nconst rect = document.createElementNS(NS.SVG, 'rect');\nconst rxform = rect.transform.baseVal;\nconst t1 = svg.createSVGTransform();\nrxform.appendItem(t1);\nconst r1 = rxform.getItem(0);\n// Todo: Do frame-independent instance checking\nreturn r1 instanceof SVGTransform && t1 instanceof SVGTransform &&\n\tr1.type === t1.type && r1.angle === t1.angle &&\n\tr1.matrix.a === t1.matrix.a &&\n\tr1.matrix.b === t1.matrix.b &&\n\tr1.matrix.c === t1.matrix.c &&\n\tr1.matrix.d === t1.matrix.d &&\n\tr1.matrix.e === t1.matrix.e &&\n\tr1.matrix.f === t1.matrix.f;\n}());\n\n// Public API\n\nexport const isOpera = () => isOpera_;\nexport const isWebkit = () => isWebkit_;\nexport const isGecko = () => isGecko_;\nexport const isIE = () => isIE_;\nexport const isChrome = () => isChrome_;\nexport const isWindows = () => isWindows_;\nexport const isMac = () => isMac_;\nexport const isTouch = () => isTouch_;\n\nexport const supportsSelectors = () => supportsSelectors_;\nexport const supportsXpath = () => supportsXpath_;\n\nexport const supportsPathReplaceItem = () => supportsPathReplaceItem_;\nexport const supportsPathInsertItemBefore = () => supportsPathInsertItemBefore_;\nexport const supportsPathBBox = () => supportsPathBBox_;\nexport const supportsHVLineContainerBBox = () => supportsHVLineContainerBBox_;\nexport const supportsGoodTextCharPos = () => supportsGoodTextCharPos_;\nexport const supportsEditableText = () => supportsEditableText_;\nexport const supportsGoodDecimals = () => supportsGoodDecimals_;\nexport const supportsNonScalingStroke = () => supportsNonScalingStroke_;\nexport const supportsNativeTransformLists = () => supportsNativeSVGTransformLists_;\n\n// Using for unit testing\nexport const disableSupportsNativeTransformLists = () => {\n supportsNativeSVGTransformLists_ = false;\n};\n","/**\n * A class to parse color values\n * @author Stoyan Stefanov \n * @link https://www.phpied.com/rgb-color-parser-in-javascript/\n * @license MIT\n */\nexport default function RGBColor (colorString) {\n this.ok = false;\n\n // strip any leading #\n if (colorString.charAt(0) === '#') { // remove # if any\n colorString = colorString.substr(1, 6);\n }\n\n colorString = colorString.replace(/ /g, '');\n colorString = colorString.toLowerCase();\n\n // before getting into regexps, try simple matches\n // and overwrite the input\n const simpleColors = {\n aliceblue: 'f0f8ff',\n antiquewhite: 'faebd7',\n aqua: '00ffff',\n aquamarine: '7fffd4',\n azure: 'f0ffff',\n beige: 'f5f5dc',\n bisque: 'ffe4c4',\n black: '000000',\n blanchedalmond: 'ffebcd',\n blue: '0000ff',\n blueviolet: '8a2be2',\n brown: 'a52a2a',\n burlywood: 'deb887',\n cadetblue: '5f9ea0',\n chartreuse: '7fff00',\n chocolate: 'd2691e',\n coral: 'ff7f50',\n cornflowerblue: '6495ed',\n cornsilk: 'fff8dc',\n crimson: 'dc143c',\n cyan: '00ffff',\n darkblue: '00008b',\n darkcyan: '008b8b',\n darkgoldenrod: 'b8860b',\n darkgray: 'a9a9a9',\n darkgreen: '006400',\n darkkhaki: 'bdb76b',\n darkmagenta: '8b008b',\n darkolivegreen: '556b2f',\n darkorange: 'ff8c00',\n darkorchid: '9932cc',\n darkred: '8b0000',\n darksalmon: 'e9967a',\n darkseagreen: '8fbc8f',\n darkslateblue: '483d8b',\n darkslategray: '2f4f4f',\n darkturquoise: '00ced1',\n darkviolet: '9400d3',\n deeppink: 'ff1493',\n deepskyblue: '00bfff',\n dimgray: '696969',\n dodgerblue: '1e90ff',\n feldspar: 'd19275',\n firebrick: 'b22222',\n floralwhite: 'fffaf0',\n forestgreen: '228b22',\n fuchsia: 'ff00ff',\n gainsboro: 'dcdcdc',\n ghostwhite: 'f8f8ff',\n gold: 'ffd700',\n goldenrod: 'daa520',\n gray: '808080',\n green: '008000',\n greenyellow: 'adff2f',\n honeydew: 'f0fff0',\n hotpink: 'ff69b4',\n indianred: 'cd5c5c',\n indigo: '4b0082',\n ivory: 'fffff0',\n khaki: 'f0e68c',\n lavender: 'e6e6fa',\n lavenderblush: 'fff0f5',\n lawngreen: '7cfc00',\n lemonchiffon: 'fffacd',\n lightblue: 'add8e6',\n lightcoral: 'f08080',\n lightcyan: 'e0ffff',\n lightgoldenrodyellow: 'fafad2',\n lightgrey: 'd3d3d3',\n lightgreen: '90ee90',\n lightpink: 'ffb6c1',\n lightsalmon: 'ffa07a',\n lightseagreen: '20b2aa',\n lightskyblue: '87cefa',\n lightslateblue: '8470ff',\n lightslategray: '778899',\n lightsteelblue: 'b0c4de',\n lightyellow: 'ffffe0',\n lime: '00ff00',\n limegreen: '32cd32',\n linen: 'faf0e6',\n magenta: 'ff00ff',\n maroon: '800000',\n mediumaquamarine: '66cdaa',\n mediumblue: '0000cd',\n mediumorchid: 'ba55d3',\n mediumpurple: '9370d8',\n mediumseagreen: '3cb371',\n mediumslateblue: '7b68ee',\n mediumspringgreen: '00fa9a',\n mediumturquoise: '48d1cc',\n mediumvioletred: 'c71585',\n midnightblue: '191970',\n mintcream: 'f5fffa',\n mistyrose: 'ffe4e1',\n moccasin: 'ffe4b5',\n navajowhite: 'ffdead',\n navy: '000080',\n oldlace: 'fdf5e6',\n olive: '808000',\n olivedrab: '6b8e23',\n orange: 'ffa500',\n orangered: 'ff4500',\n orchid: 'da70d6',\n palegoldenrod: 'eee8aa',\n palegreen: '98fb98',\n paleturquoise: 'afeeee',\n palevioletred: 'd87093',\n papayawhip: 'ffefd5',\n peachpuff: 'ffdab9',\n peru: 'cd853f',\n pink: 'ffc0cb',\n plum: 'dda0dd',\n powderblue: 'b0e0e6',\n purple: '800080',\n red: 'ff0000',\n rosybrown: 'bc8f8f',\n royalblue: '4169e1',\n saddlebrown: '8b4513',\n salmon: 'fa8072',\n sandybrown: 'f4a460',\n seagreen: '2e8b57',\n seashell: 'fff5ee',\n sienna: 'a0522d',\n silver: 'c0c0c0',\n skyblue: '87ceeb',\n slateblue: '6a5acd',\n slategray: '708090',\n snow: 'fffafa',\n springgreen: '00ff7f',\n steelblue: '4682b4',\n tan: 'd2b48c',\n teal: '008080',\n thistle: 'd8bfd8',\n tomato: 'ff6347',\n turquoise: '40e0d0',\n violet: 'ee82ee',\n violetred: 'd02090',\n wheat: 'f5deb3',\n white: 'ffffff',\n whitesmoke: 'f5f5f5',\n yellow: 'ffff00',\n yellowgreen: '9acd32'\n };\n for (const key in simpleColors) {\n if (simpleColors.hasOwnProperty(key)) {\n if (colorString === key) {\n colorString = simpleColors[key];\n }\n }\n }\n // emd of simple type-in colors\n\n // array of color definition objects\n const colorDefs = [\n {\n re: /^rgb\\((\\d{1,3}),\\s*(\\d{1,3}),\\s*(\\d{1,3})\\)$/,\n example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],\n process (bits) {\n return [\n parseInt(bits[1], 10),\n parseInt(bits[2], 10),\n parseInt(bits[3], 10)\n ];\n }\n },\n {\n re: /^(\\w{2})(\\w{2})(\\w{2})$/,\n example: ['#00ff00', '336699'],\n process (bits) {\n return [\n parseInt(bits[1], 16),\n parseInt(bits[2], 16),\n parseInt(bits[3], 16)\n ];\n }\n },\n {\n re: /^(\\w{1})(\\w{1})(\\w{1})$/,\n example: ['#fb0', 'f0f'],\n process (bits) {\n return [\n parseInt(bits[1] + bits[1], 16),\n parseInt(bits[2] + bits[2], 16),\n parseInt(bits[3] + bits[3], 16)\n ];\n }\n }\n ];\n\n // search through the definitions to find a match\n for (let i = 0; i < colorDefs.length; i++) {\n const {re} = colorDefs[i];\n const processor = colorDefs[i].process;\n const bits = re.exec(colorString);\n if (bits) {\n const channels = processor(bits);\n this.r = channels[0];\n this.g = channels[1];\n this.b = channels[2];\n this.ok = true;\n }\n }\n\n // validate/cleanup values\n this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);\n this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);\n this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);\n\n // some getters\n this.toRGB = function () {\n return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';\n };\n this.toHex = function () {\n let r = this.r.toString(16);\n let g = this.g.toString(16);\n let b = this.b.toString(16);\n if (r.length === 1) { r = '0' + r; }\n if (g.length === 1) { g = '0' + g; }\n if (b.length === 1) { b = '0' + b; }\n return '#' + r + g + b;\n };\n\n // help\n this.getHelpXML = function () {\n const examples = [];\n // add regexps\n for (let i = 0; i < colorDefs.length; i++) {\n const {example} = colorDefs[i];\n for (let j = 0; j < example.length; j++) {\n examples[examples.length] = example[j];\n }\n }\n // add type-in colors\n for (const sc in simpleColors) {\n if (simpleColors.hasOwnProperty(sc)) {\n examples[examples.length] = sc;\n }\n }\n\n const xml = document.createElement('ul');\n xml.setAttribute('id', 'rgbcolor-examples');\n for (let i = 0; i < examples.length; i++) {\n try {\n const listItem = document.createElement('li');\n const listColor = new RGBColor(examples[i]);\n const exampleDiv = document.createElement('div');\n exampleDiv.style.cssText =\n 'margin: 3px; ' +\n 'border: 1px solid black; ' +\n 'background:' + listColor.toHex() + '; ' +\n 'color:' + listColor.toHex()\n ;\n exampleDiv.appendChild(document.createTextNode('test'));\n const listItemValue = document.createTextNode(\n ' ' + examples[i] + ' -> ' + listColor.toRGB() + ' -> ' + listColor.toHex()\n );\n listItem.appendChild(exampleDiv);\n listItem.appendChild(listItemValue);\n xml.appendChild(listItem);\n } catch (e) {}\n }\n return xml;\n };\n}\n","/**\n * jQuery module to work with SVG.\n *\n * Licensed under the MIT License\n *\n */\n\n// This fixes $(...).attr() to work as expected with SVG elements.\n// Does not currently use *AttributeNS() since we rarely need that.\n\n// See https://api.jquery.com/attr/ for basic documentation of .attr()\n\n// Additional functionality:\n// - When getting attributes, a string that's a number is returned as type number.\n// - If an array is supplied as the first parameter, multiple values are returned\n// as an object with values for each given attribute\n\nexport default function ($) {\n const proxied = $.fn.attr,\n svgns = 'http://www.w3.org/2000/svg';\n $.fn.attr = function (key, value) {\n const len = this.length;\n if (!len) { return proxied.apply(this, arguments); }\n for (let i = 0; i < len; ++i) {\n const elem = this[i];\n // set/get SVG attribute\n if (elem.namespaceURI === svgns) {\n // Setting attribute\n if (value !== undefined) {\n elem.setAttribute(key, value);\n } else if (Array.isArray(key)) {\n // Getting attributes from array\n const obj = {};\n let j = key.length;\n\n while (j--) {\n const aname = key[j];\n let attr = elem.getAttribute(aname);\n // This returns a number when appropriate\n if (attr || attr === '0') {\n attr = isNaN(attr) ? attr : (attr - 0);\n }\n obj[aname] = attr;\n }\n return obj;\n }\n if (typeof key === 'object') {\n // Setting attributes from object\n for (const v in key) {\n elem.setAttribute(v, key[v]);\n }\n // Getting attribute\n } else {\n let attr = elem.getAttribute(key);\n if (attr || attr === '0') {\n attr = isNaN(attr) ? attr : (attr - 0);\n }\n return attr;\n }\n } else {\n return proxied.apply(this, arguments);\n }\n }\n return this;\n };\n return $;\n}\n","// MIT License\n// From: https://github.com/uupaa/dynamic-import-polyfill/blob/master/importModule.js\n\nfunction toAbsoluteURL(url) {\n const a = document.createElement(\"a\");\n a.setAttribute(\"href\", url); // \n return a.cloneNode(false).href; // -> \"http://example.com/hoge.html\"\n}\n\n// My own addition\nexport function importScript(url) {\n return new Promise((resolve, reject) => {\n const script = document.createElement(\"script\");\n const destructor = () => {\n script.onerror = null;\n script.onload = null;\n script.remove();\n script.src = \"\";\n };\n script.defer = \"defer\";\n script.onerror = () => {\n reject(new Error(`Failed to import: ${url}`));\n destructor();\n };\n script.onload = () => {\n resolve();\n destructor();\n };\n script.src = url;\n\n document.head.appendChild(script);\n });\n}\n\nexport function importModule(url) {\n return new Promise((resolve, reject) => {\n const vector = \"$importModule$\" + Math.random().toString(32).slice(2);\n const script = document.createElement(\"script\");\n const destructor = () => {\n delete window[vector];\n script.onerror = null;\n script.onload = null;\n script.remove();\n URL.revokeObjectURL(script.src);\n script.src = \"\";\n };\n script.defer = \"defer\";\n script.type = \"module\";\n script.onerror = () => {\n reject(new Error(`Failed to import: ${url}`));\n destructor();\n };\n script.onload = () => {\n resolve(window[vector]);\n destructor();\n };\n const absURL = toAbsoluteURL(url);\n const loader = `import * as m from \"${absURL}\"; window.${vector} = m;`; // export Module\n const blob = new Blob([loader], { type: \"text/javascript\" });\n script.src = URL.createObjectURL(blob);\n\n document.head.appendChild(script);\n });\n}\n\nexport default importModule;\n","/**\n * SVGTransformList\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Alexis Deveria\n * Copyright(c) 2010 Jeff Schiller\n */\n\nimport {NS} from './svgedit.js';\nimport {supportsNativeTransformLists} from './browser.js';\n\nconst svgroot = document.createElementNS(NS.SVG, 'svg');\n\n// Helper function.\nfunction transformToString (xform) {\n const m = xform.matrix;\n let text = '';\n switch (xform.type) {\n case 1: // MATRIX\n text = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')';\n break;\n case 2: // TRANSLATE\n text = 'translate(' + m.e + ',' + m.f + ')';\n break;\n case 3: // SCALE\n if (m.a === m.d) {\n text = 'scale(' + m.a + ')';\n } else {\n text = 'scale(' + m.a + ',' + m.d + ')';\n }\n break;\n case 4: { // ROTATE\n let cx = 0;\n let cy = 0;\n // this prevents divide by zero\n if (xform.angle !== 0) {\n const K = 1 - m.a;\n cy = (K * m.f + m.b * m.e) / (K * K + m.b * m.b);\n cx = (m.e - m.b * cy) / K;\n }\n text = 'rotate(' + xform.angle + ' ' + cx + ',' + cy + ')';\n break;\n }\n }\n return text;\n}\n\n/**\n * Map of SVGTransformList objects.\n */\nlet listMap_ = {};\n\n// **************************************************************************************\n// SVGTransformList implementation for Webkit\n// These methods do not currently raise any exceptions.\n// These methods also do not check that transforms are being inserted. This is basically\n// implementing as much of SVGTransformList that we need to get the job done.\n//\n// interface SVGEditTransformList {\n// attribute unsigned long numberOfItems;\n// void clear ( )\n// SVGTransform initialize ( in SVGTransform newItem )\n// SVGTransform getItem ( in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR)\n// SVGTransform insertItemBefore ( in SVGTransform newItem, in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR)\n// SVGTransform replaceItem ( in SVGTransform newItem, in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR)\n// SVGTransform removeItem ( in unsigned long index ) (DOES NOT THROW DOMException, INDEX_SIZE_ERR)\n// SVGTransform appendItem ( in SVGTransform newItem )\n// NOT IMPLEMENTED: SVGTransform createSVGTransformFromMatrix ( in SVGMatrix matrix );\n// NOT IMPLEMENTED: SVGTransform consolidate ( );\n// }\n// **************************************************************************************\nexport class SVGTransformList {\n constructor (elem) {\n this._elem = elem || null;\n this._xforms = [];\n // TODO: how do we capture the undo-ability in the changed transform list?\n this._update = function () {\n let tstr = '';\n /* const concatMatrix = */ svgroot.createSVGMatrix();\n for (let i = 0; i < this.numberOfItems; ++i) {\n const xform = this._list.getItem(i);\n tstr += transformToString(xform) + ' ';\n }\n this._elem.setAttribute('transform', tstr);\n };\n this._list = this;\n this._init = function () {\n // Transform attribute parser\n let str = this._elem.getAttribute('transform');\n if (!str) { return; }\n\n // TODO: Add skew support in future\n const re = /\\s*((scale|matrix|rotate|translate)\\s*\\(.*?\\))\\s*,?\\s*/;\n let m = true;\n while (m) {\n m = str.match(re);\n str = str.replace(re, '');\n if (m && m[1]) {\n const x = m[1];\n const bits = x.split(/\\s*\\(/);\n const name = bits[0];\n const valBits = bits[1].match(/\\s*(.*?)\\s*\\)/);\n valBits[1] = valBits[1].replace(/(\\d)-/g, '$1 -');\n const valArr = valBits[1].split(/[, ]+/);\n const letters = 'abcdef'.split('');\n const mtx = svgroot.createSVGMatrix();\n Object.values(valArr).forEach(function (item, i) {\n valArr[i] = parseFloat(item);\n if (name === 'matrix') {\n mtx[letters[i]] = valArr[i];\n }\n });\n const xform = svgroot.createSVGTransform();\n const fname = 'set' + name.charAt(0).toUpperCase() + name.slice(1);\n const values = name === 'matrix' ? [mtx] : valArr;\n\n if (name === 'scale' && values.length === 1) {\n values.push(values[0]);\n } else if (name === 'translate' && values.length === 1) {\n values.push(0);\n } else if (name === 'rotate' && values.length === 1) {\n values.push(0, 0);\n }\n xform[fname].apply(xform, values);\n this._list.appendItem(xform);\n }\n }\n };\n this._removeFromOtherLists = function (item) {\n if (item) {\n // Check if this transform is already in a transformlist, and\n // remove it if so.\n let found = false;\n for (const id in listMap_) {\n const tl = listMap_[id];\n for (let i = 0, len = tl._xforms.length; i < len; ++i) {\n if (tl._xforms[i] === item) {\n found = true;\n tl.removeItem(i);\n break;\n }\n }\n if (found) {\n break;\n }\n }\n }\n };\n\n this.numberOfItems = 0;\n this.clear = function () {\n this.numberOfItems = 0;\n this._xforms = [];\n };\n\n this.initialize = function (newItem) {\n this.numberOfItems = 1;\n this._removeFromOtherLists(newItem);\n this._xforms = [newItem];\n };\n\n this.getItem = function (index) {\n if (index < this.numberOfItems && index >= 0) {\n return this._xforms[index];\n }\n const err = new Error('DOMException with code=INDEX_SIZE_ERR');\n err.code = 1;\n throw err;\n };\n\n this.insertItemBefore = function (newItem, index) {\n let retValue = null;\n if (index >= 0) {\n if (index < this.numberOfItems) {\n this._removeFromOtherLists(newItem);\n const newxforms = new Array(this.numberOfItems + 1);\n // TODO: use array copying and slicing\n let i;\n for (i = 0; i < index; ++i) {\n newxforms[i] = this._xforms[i];\n }\n newxforms[i] = newItem;\n for (let j = i + 1; i < this.numberOfItems; ++j, ++i) {\n newxforms[j] = this._xforms[i];\n }\n this.numberOfItems++;\n this._xforms = newxforms;\n retValue = newItem;\n this._list._update();\n } else {\n retValue = this._list.appendItem(newItem);\n }\n }\n return retValue;\n };\n\n this.replaceItem = function (newItem, index) {\n let retValue = null;\n if (index < this.numberOfItems && index >= 0) {\n this._removeFromOtherLists(newItem);\n this._xforms[index] = newItem;\n retValue = newItem;\n this._list._update();\n }\n return retValue;\n };\n\n this.removeItem = function (index) {\n if (index < this.numberOfItems && index >= 0) {\n const retValue = this._xforms[index];\n const newxforms = new Array(this.numberOfItems - 1);\n let i;\n for (i = 0; i < index; ++i) {\n newxforms[i] = this._xforms[i];\n }\n for (let j = i; j < this.numberOfItems - 1; ++j, ++i) {\n newxforms[j] = this._xforms[i + 1];\n }\n this.numberOfItems--;\n this._xforms = newxforms;\n this._list._update();\n return retValue;\n }\n const err = new Error('DOMException with code=INDEX_SIZE_ERR');\n err.code = 1;\n throw err;\n };\n\n this.appendItem = function (newItem) {\n this._removeFromOtherLists(newItem);\n this._xforms.push(newItem);\n this.numberOfItems++;\n this._list._update();\n return newItem;\n };\n }\n}\n\nexport const resetListMap = function () {\n listMap_ = {};\n};\n\n/**\n * Removes transforms of the given element from the map.\n * Parameters:\n * elem - a DOM Element\n */\nexport let removeElementFromListMap = function (elem) {\n if (elem.id && listMap_[elem.id]) {\n delete listMap_[elem.id];\n }\n};\n\n/**\n* Returns an object that behaves like a SVGTransformList for the given DOM element\n* @param elem - DOM element to get a transformlist from\n*/\nexport const getTransformList = function (elem) {\n if (!supportsNativeTransformLists()) {\n const id = elem.id || 'temp';\n let t = listMap_[id];\n if (!t || id === 'temp') {\n listMap_[id] = new SVGTransformList(elem);\n listMap_[id]._init();\n t = listMap_[id];\n }\n return t;\n }\n if (elem.transform) {\n return elem.transform.baseVal;\n }\n if (elem.gradientTransform) {\n return elem.gradientTransform.baseVal;\n }\n if (elem.patternTransform) {\n return elem.patternTransform.baseVal;\n }\n\n return null;\n};\n\n// For unit-testing\nexport const changeRemoveElementFromListMap = function (cb) {\n removeElementFromListMap = cb;\n};\n","/**\n * Package: svgedit.units\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Alexis Deveria\n * Copyright(c) 2010 Jeff Schiller\n */\n\nimport {NS} from './svgedit.js';\n\nconst wAttrs = ['x', 'x1', 'cx', 'rx', 'width'];\nconst hAttrs = ['y', 'y1', 'cy', 'ry', 'height'];\nconst unitAttrs = ['r', 'radius', ...wAttrs, ...hAttrs];\n// unused\n/*\nconst unitNumMap = {\n '%': 2,\n em: 3,\n ex: 4,\n px: 5,\n cm: 6,\n mm: 7,\n in: 8,\n pt: 9,\n pc: 10\n};\n*/\n// Container of elements.\nlet elementContainer_;\n\n/**\n * Stores mapping of unit type to user coordinates.\n */\nlet typeMap_ = {};\n\n/**\n * ElementContainer interface\n *\n * function getBaseUnit() - Returns a string of the base unit type of the container ('em')\n * function getElement() - Returns an element in the container given an id\n * function getHeight() - Returns the container's height\n * function getWidth() - Returns the container's width\n * function getRoundDigits() - Returns the number of digits number should be rounded to\n */\n\n/**\n * Initializes this module.\n *\n * @param elementContainer - An object implementing the ElementContainer interface.\n */\nexport const init = function (elementContainer) {\n elementContainer_ = elementContainer;\n\n // Get correct em/ex values by creating a temporary SVG.\n const svg = document.createElementNS(NS.SVG, 'svg');\n document.body.appendChild(svg);\n const rect = document.createElementNS(NS.SVG, 'rect');\n rect.setAttribute('width', '1em');\n rect.setAttribute('height', '1ex');\n rect.setAttribute('x', '1in');\n svg.appendChild(rect);\n const bb = rect.getBBox();\n document.body.removeChild(svg);\n\n const inch = bb.x;\n typeMap_ = {\n em: bb.width,\n ex: bb.height,\n in: inch,\n cm: inch / 2.54,\n mm: inch / 25.4,\n pt: inch / 72,\n pc: inch / 6,\n px: 1,\n '%': 0\n };\n};\n\n/**\n* Group: Unit conversion functions\n*/\n\n/**\n* @returns The unit object with values for each unit\n*/\nexport const getTypeMap = function () {\n return typeMap_;\n};\n\n/**\n* Rounds a given value to a float with number of digits defined in save_options\n*\n* @param val - The value as a String, Number or Array of two numbers to be rounded\n*\n* @returns\n* If a string/number was given, returns a Float. If an array, return a string\n* with comma-separated floats\n*/\nexport const shortFloat = function (val) {\n const digits = elementContainer_.getRoundDigits();\n if (!isNaN(val)) {\n // Note that + converts to Number\n return +((+val).toFixed(digits));\n }\n if (Array.isArray(val)) {\n return shortFloat(val[0]) + ',' + shortFloat(val[1]);\n }\n return parseFloat(val).toFixed(digits) - 0;\n};\n\n/**\n* Converts the number to given unit or baseUnit\n* @returns {number}\n*/\nexport const convertUnit = function (val, unit) {\n unit = unit || elementContainer_.getBaseUnit();\n // baseVal.convertToSpecifiedUnits(unitNumMap[unit]);\n // const val = baseVal.valueInSpecifiedUnits;\n // baseVal.convertToSpecifiedUnits(1);\n return shortFloat(val / typeMap_[unit]);\n};\n\n/**\n* Sets an element's attribute based on the unit in its current value.\n*\n* @param elem - DOM element to be changed\n* @param attr - String with the name of the attribute associated with the value\n* @param val - String with the attribute value to convert\n*/\nexport const setUnitAttr = function (elem, attr, val) {\n // if (!isNaN(val)) {\n // New value is a number, so check currently used unit\n // const oldVal = elem.getAttribute(attr);\n\n // Enable this for alternate mode\n // if (oldVal !== null && (isNaN(oldVal) || elementContainer_.getBaseUnit() !== 'px')) {\n // // Old value was a number, so get unit, then convert\n // let unit;\n // if (oldVal.substr(-1) === '%') {\n // const res = getResolution();\n // unit = '%';\n // val *= 100;\n // if (wAttrs.includes(attr)) {\n // val = val / res.w;\n // } else if (hAttrs.includes(attr)) {\n // val = val / res.h;\n // } else {\n // return val / Math.sqrt((res.w*res.w) + (res.h*res.h))/Math.sqrt(2);\n // }\n // } else {\n // if (elementContainer_.getBaseUnit() !== 'px') {\n // unit = elementContainer_.getBaseUnit();\n // } else {\n // unit = oldVal.substr(-2);\n // }\n // val = val / typeMap_[unit];\n // }\n //\n // val += unit;\n // }\n // }\n elem.setAttribute(attr, val);\n};\n\nconst attrsToConvert = {\n line: ['x1', 'x2', 'y1', 'y2'],\n circle: ['cx', 'cy', 'r'],\n ellipse: ['cx', 'cy', 'rx', 'ry'],\n foreignObject: ['x', 'y', 'width', 'height'],\n rect: ['x', 'y', 'width', 'height'],\n image: ['x', 'y', 'width', 'height'],\n use: ['x', 'y', 'width', 'height'],\n text: ['x', 'y']\n};\n\n/**\n* Converts all applicable attributes to the configured baseUnit\n* @param element - A DOM element whose attributes should be converted\n*/\nexport const convertAttrs = function (element) {\n const elName = element.tagName;\n const unit = elementContainer_.getBaseUnit();\n const attrs = attrsToConvert[elName];\n if (!attrs) { return; }\n\n const len = attrs.length;\n for (let i = 0; i < len; i++) {\n const attr = attrs[i];\n const cur = element.getAttribute(attr);\n if (cur) {\n if (!isNaN(cur)) {\n element.setAttribute(attr, (cur / typeMap_[unit]) + unit);\n }\n // else {\n // Convert existing?\n // }\n }\n }\n};\n\n/**\n* Converts given values to numbers. Attributes must be supplied in\n* case a percentage is given\n*\n* @param attr - String with the name of the attribute associated with the value\n* @param val - String with the attribute value to convert\n*/\nexport const convertToNum = function (attr, val) {\n // Return a number if that's what it already is\n if (!isNaN(val)) { return val - 0; }\n if (val.substr(-1) === '%') {\n // Deal with percentage, depends on attribute\n const num = val.substr(0, val.length - 1) / 100;\n const width = elementContainer_.getWidth();\n const height = elementContainer_.getHeight();\n\n if (wAttrs.includes(attr)) {\n return num * width;\n }\n if (hAttrs.includes(attr)) {\n return num * height;\n }\n return num * Math.sqrt((width * width) + (height * height)) / Math.sqrt(2);\n }\n const unit = val.substr(-2);\n const num = val.substr(0, val.length - 2);\n // Note that this multiplication turns the string into a number\n return num * typeMap_[unit];\n};\n\n/**\n* Check if an attribute's value is in a valid format\n* @param attr - String with the name of the attribute associated with the value\n* @param val - String with the attribute value to check\n*/\nexport const isValidUnit = function (attr, val, selectedElement) {\n let valid = false;\n if (unitAttrs.includes(attr)) {\n // True if it's just a number\n if (!isNaN(val)) {\n valid = true;\n } else {\n // Not a number, check if it has a valid unit\n val = val.toLowerCase();\n Object.keys(typeMap_).forEach((unit) => {\n if (valid) { return; }\n const re = new RegExp('^-?[\\\\d\\\\.]+' + unit + '$');\n if (re.test(val)) { valid = true; }\n });\n }\n } else if (attr === 'id') {\n // if we're trying to change the id, make sure it's not already present in the doc\n // and the id value is valid.\n\n let result = false;\n // because getElem() can throw an exception in the case of an invalid id\n // (according to https://www.w3.org/TR/xml-id/ IDs must be a NCName)\n // we wrap it in an exception and only return true if the ID was valid and\n // not already present\n try {\n const elem = elementContainer_.getElement(val);\n result = (elem == null || elem === selectedElement);\n } catch (e) {}\n return result;\n }\n valid = true;\n\n return valid;\n};\n","/**\n * Package: svedit.history\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Jeff Schiller\n */\n\nimport {getHref, setHref, getRotationAngle} from './svgutils.js';\nimport {removeElementFromListMap} from './svgtransformlist.js';\n\n/**\n* Group: Undo/Redo history management\n*/\nexport const HistoryEventTypes = {\n BEFORE_APPLY: 'before_apply',\n AFTER_APPLY: 'after_apply',\n BEFORE_UNAPPLY: 'before_unapply',\n AFTER_UNAPPLY: 'after_unapply'\n};\n\n// const removedElements = {};\n\n/**\n * An interface that all command objects must implement.\n * @typedef {Object} svgedit.history.HistoryCommand\n * void apply(svgedit.history.HistoryEventHandler);\n * void unapply(svgedit.history.HistoryEventHandler);\n * Element[] elements();\n * String getText();\n *\n * static String type();\n * }\n *\n * Interface: svgedit.history.HistoryEventHandler\n * An interface for objects that will handle history events.\n *\n * interface svgedit.history.HistoryEventHandler {\n * void handleHistoryEvent(eventType, command);\n * }\n *\n * eventType is a string conforming to one of the HistoryEvent types.\n * command is an object fulfilling the HistoryCommand interface.\n */\n\n/**\n * @class svgedit.history.MoveElementCommand\n * @implements svgedit.history.HistoryCommand\n * History command for an element that had its DOM position changed\n * @param {Element} elem - The DOM element that was moved\n * @param {Element} oldNextSibling - The element's next sibling before it was moved\n * @param {Element} oldParent - The element's parent before it was moved\n * @param {string} [text] - An optional string visible to user related to this change\n*/\nexport class MoveElementCommand {\n constructor (elem, oldNextSibling, oldParent, text) {\n this.elem = elem;\n this.text = text ? ('Move ' + elem.tagName + ' to ' + text) : ('Move ' + elem.tagName);\n this.oldNextSibling = oldNextSibling;\n this.oldParent = oldParent;\n this.newNextSibling = elem.nextSibling;\n this.newParent = elem.parentNode;\n }\n getText () {\n return this.text;\n }\n type () {\n return 'svgedit.history.MoveElementCommand';\n }\n\n /**\n * Re-positions the element\n * @param {{handleHistoryEvent: function}} handler\n */\n apply (handler) {\n // TODO(codedread): Refactor this common event code into a base HistoryCommand class.\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_APPLY, this);\n }\n\n this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling);\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_APPLY, this);\n }\n }\n\n /**\n * Positions the element back to its original location\n * @param {{handleHistoryEvent: function}} handler\n */\n unapply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_UNAPPLY, this);\n }\n\n this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling);\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_UNAPPLY, this);\n }\n }\n\n /**\n * @returns {Array} Array with element associated with this command\n */\n elements () {\n return [this.elem];\n }\n}\nMoveElementCommand.type = MoveElementCommand.prototype.type;\n\n/**\n* @implements svgedit.history.HistoryCommand\n* History command for an element that was added to the DOM\n*\n* @param elem - The newly added DOM element\n* @param text - An optional string visible to user related to this change\n*/\nexport class InsertElementCommand {\n constructor (elem, text) {\n this.elem = elem;\n this.text = text || ('Create ' + elem.tagName);\n this.parent = elem.parentNode;\n this.nextSibling = this.elem.nextSibling;\n }\n\n type () {\n return 'svgedit.history.InsertElementCommand';\n }\n\n getText () {\n return this.text;\n }\n\n // Re-Inserts the new element\n apply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_APPLY, this);\n }\n\n this.elem = this.parent.insertBefore(this.elem, this.nextSibling);\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_APPLY, this);\n }\n }\n\n // Removes the element\n unapply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_UNAPPLY, this);\n }\n\n this.parent = this.elem.parentNode;\n this.elem = this.elem.parentNode.removeChild(this.elem);\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_UNAPPLY, this);\n }\n }\n\n /**\n * @returns {Array} Array with element associated with this command\n */\n elements () {\n return [this.elem];\n }\n}\nInsertElementCommand.type = InsertElementCommand.prototype.type;\n\n/**\n* @implements svgedit.history.HistoryCommand\n* History command for an element removed from the DOM\n* @param elem - The removed DOM element\n* @param oldNextSibling - The DOM element's nextSibling when it was in the DOM\n* @param oldParent - The DOM element's parent\n* @param {String} [text] - An optional string visible to user related to this change\n*/\nexport class RemoveElementCommand {\n constructor (elem, oldNextSibling, oldParent, text) {\n this.elem = elem;\n this.text = text || ('Delete ' + elem.tagName);\n this.nextSibling = oldNextSibling;\n this.parent = oldParent;\n\n // special hack for webkit: remove this element's entry in the svgTransformLists map\n removeElementFromListMap(elem);\n }\n type () {\n return 'svgedit.history.RemoveElementCommand';\n }\n\n getText () {\n return this.text;\n }\n\n // Re-removes the new element\n apply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_APPLY, this);\n }\n\n removeElementFromListMap(this.elem);\n this.parent = this.elem.parentNode;\n this.elem = this.parent.removeChild(this.elem);\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_APPLY, this);\n }\n }\n\n // Re-adds the new element\n unapply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_UNAPPLY, this);\n }\n\n removeElementFromListMap(this.elem);\n if (this.nextSibling == null) {\n if (window.console) {\n console.log('Error: reference element was lost');\n }\n }\n this.parent.insertBefore(this.elem, this.nextSibling);\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_UNAPPLY, this);\n }\n }\n\n /**\n * @returns {Array} Array with element associated with this command\n */\n elements () {\n return [this.elem];\n }\n}\nRemoveElementCommand.type = RemoveElementCommand.prototype.type;\n\n/**\n* @implements svgedit.history.HistoryCommand\n* History command to make a change to an element.\n* Usually an attribute change, but can also be textcontent.\n* @param elem - The DOM element that was changed\n* @param attrs - An object with the attributes to be changed and the values they had *before* the change\n* @param {String} text - An optional string visible to user related to this change\n*/\nexport class ChangeElementCommand {\n constructor (elem, attrs, text) {\n this.elem = elem;\n this.text = text ? ('Change ' + elem.tagName + ' ' + text) : ('Change ' + elem.tagName);\n this.newValues = {};\n this.oldValues = attrs;\n for (const attr in attrs) {\n if (attr === '#text') {\n this.newValues[attr] = elem.textContent;\n } else if (attr === '#href') {\n this.newValues[attr] = getHref(elem);\n } else {\n this.newValues[attr] = elem.getAttribute(attr);\n }\n }\n }\n type () {\n return 'svgedit.history.ChangeElementCommand';\n }\n\n getText () {\n return this.text;\n }\n\n // Performs the stored change action\n apply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_APPLY, this);\n }\n\n let bChangedTransform = false;\n for (const attr in this.newValues) {\n if (this.newValues[attr]) {\n if (attr === '#text') {\n this.elem.textContent = this.newValues[attr];\n } else if (attr === '#href') {\n setHref(this.elem, this.newValues[attr]);\n } else {\n this.elem.setAttribute(attr, this.newValues[attr]);\n }\n } else {\n if (attr === '#text') {\n this.elem.textContent = '';\n } else {\n this.elem.setAttribute(attr, '');\n this.elem.removeAttribute(attr);\n }\n }\n\n if (attr === 'transform') { bChangedTransform = true; }\n }\n\n // relocate rotational transform, if necessary\n if (!bChangedTransform) {\n const angle = getRotationAngle(this.elem);\n if (angle) {\n const bbox = this.elem.getBBox();\n const cx = bbox.x + bbox.width / 2,\n cy = bbox.y + bbox.height / 2;\n const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('');\n if (rotate !== this.elem.getAttribute('transform')) {\n this.elem.setAttribute('transform', rotate);\n }\n }\n }\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_APPLY, this);\n }\n\n return true;\n }\n\n // Reverses the stored change action\n unapply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_UNAPPLY, this);\n }\n\n let bChangedTransform = false;\n for (const attr in this.oldValues) {\n if (this.oldValues[attr]) {\n if (attr === '#text') {\n this.elem.textContent = this.oldValues[attr];\n } else if (attr === '#href') {\n setHref(this.elem, this.oldValues[attr]);\n } else {\n this.elem.setAttribute(attr, this.oldValues[attr]);\n }\n } else {\n if (attr === '#text') {\n this.elem.textContent = '';\n } else {\n this.elem.removeAttribute(attr);\n }\n }\n if (attr === 'transform') { bChangedTransform = true; }\n }\n // relocate rotational transform, if necessary\n if (!bChangedTransform) {\n const angle = getRotationAngle(this.elem);\n if (angle) {\n const bbox = this.elem.getBBox();\n const cx = bbox.x + bbox.width / 2,\n cy = bbox.y + bbox.height / 2;\n const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('');\n if (rotate !== this.elem.getAttribute('transform')) {\n this.elem.setAttribute('transform', rotate);\n }\n }\n }\n\n // Remove transformlist to prevent confusion that causes bugs like 575.\n removeElementFromListMap(this.elem);\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_UNAPPLY, this);\n }\n\n return true;\n }\n\n /**\n * @returns {Array} Array with element associated with this command\n */\n elements () {\n return [this.elem];\n }\n}\nChangeElementCommand.type = ChangeElementCommand.prototype.type;\n\n// TODO: create a 'typing' command object that tracks changes in text\n// if a new Typing command is created and the top command on the stack is also a Typing\n// and they both affect the same element, then collapse the two commands into one\n\n/**\n* @implements svgedit.history.HistoryCommand\n* History command that can contain/execute multiple other commands\n* @param {String} [text] - An optional string visible to user related to this change\n*/\nexport class BatchCommand {\n constructor (text) {\n this.text = text || 'Batch Command';\n this.stack = [];\n }\n\n type () {\n return 'svgedit.history.BatchCommand';\n }\n\n getText () {\n return this.text;\n }\n\n // Runs \"apply\" on all subcommands\n apply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_APPLY, this);\n }\n\n const len = this.stack.length;\n for (let i = 0; i < len; ++i) {\n this.stack[i].apply(handler);\n }\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_APPLY, this);\n }\n }\n\n // Runs \"unapply\" on all subcommands\n unapply (handler) {\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.BEFORE_UNAPPLY, this);\n }\n\n for (let i = this.stack.length - 1; i >= 0; i--) {\n this.stack[i].unapply(handler);\n }\n\n if (handler) {\n handler.handleHistoryEvent(HistoryEventTypes.AFTER_UNAPPLY, this);\n }\n }\n\n // Iterate through all our subcommands and returns all the elements we are changing\n elements () {\n const elems = [];\n let cmd = this.stack.length;\n while (cmd--) {\n const thisElems = this.stack[cmd].elements();\n let elem = thisElems.length;\n while (elem--) {\n if (!elems.includes(thisElems[elem])) { elems.push(thisElems[elem]); }\n }\n }\n return elems;\n }\n\n /**\n * Adds a given command to the history stack\n * @param cmd - The undo command object to add\n */\n addSubCommand (cmd) {\n this.stack.push(cmd);\n }\n\n /**\n * @returns {Boolean} Indicates whether or not the batch command is empty\n */\n isEmpty () {\n return !this.stack.length;\n }\n}\nBatchCommand.type = BatchCommand.prototype.type;\n\n/**\n* @param historyEventHandler - an object that conforms to the HistoryEventHandler interface\n* (see above)\n*/\nexport class UndoManager {\n constructor (historyEventHandler) {\n this.handler_ = historyEventHandler || null;\n this.undoStackPointer = 0;\n this.undoStack = [];\n\n // this is the stack that stores the original values, the elements and\n // the attribute name for begin/finish\n this.undoChangeStackPointer = -1;\n this.undoableChangeStack = [];\n }\n\n // Resets the undo stack, effectively clearing the undo/redo history\n resetUndoStack () {\n this.undoStack = [];\n this.undoStackPointer = 0;\n }\n\n /**\n * @returns {Number} Integer with the current size of the undo history stack\n */\n getUndoStackSize () {\n return this.undoStackPointer;\n }\n\n /**\n * @returns {Number} Integer with the current size of the redo history stack\n */\n getRedoStackSize () {\n return this.undoStack.length - this.undoStackPointer;\n }\n\n /**\n * @returns {String} String associated with the next undo command\n */\n getNextUndoCommandText () {\n return this.undoStackPointer > 0 ? this.undoStack[this.undoStackPointer - 1].getText() : '';\n }\n\n /**\n * @returns {String} String associated with the next redo command\n */\n getNextRedoCommandText () {\n return this.undoStackPointer < this.undoStack.length ? this.undoStack[this.undoStackPointer].getText() : '';\n }\n\n // Performs an undo step\n undo () {\n if (this.undoStackPointer > 0) {\n const cmd = this.undoStack[--this.undoStackPointer];\n cmd.unapply(this.handler_);\n }\n }\n\n // Performs a redo step\n redo () {\n if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {\n const cmd = this.undoStack[this.undoStackPointer++];\n cmd.apply(this.handler_);\n }\n }\n\n /**\n * Adds a command object to the undo history stack\n * @param cmd - The command object to add\n */\n addCommandToHistory (cmd) {\n // FIXME: we MUST compress consecutive text changes to the same element\n // (right now each keystroke is saved as a separate command that includes the\n // entire text contents of the text element)\n // TODO: consider limiting the history that we store here (need to do some slicing)\n\n // if our stack pointer is not at the end, then we have to remove\n // all commands after the pointer and insert the new command\n if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {\n this.undoStack = this.undoStack.splice(0, this.undoStackPointer);\n }\n this.undoStack.push(cmd);\n this.undoStackPointer = this.undoStack.length;\n }\n\n /**\n * This function tells the canvas to remember the old values of the\n * attrName attribute for each element sent in. The elements and values\n * are stored on a stack, so the next call to finishUndoableChange() will\n * pop the elements and old values off the stack, gets the current values\n * from the DOM and uses all of these to construct the undo-able command.\n * @param attrName - The name of the attribute being changed\n * @param elems - Array of DOM elements being changed\n */\n beginUndoableChange (attrName, elems) {\n const p = ++this.undoChangeStackPointer;\n let i = elems.length;\n const oldValues = new Array(i), elements = new Array(i);\n while (i--) {\n const elem = elems[i];\n if (elem == null) { continue; }\n elements[i] = elem;\n oldValues[i] = elem.getAttribute(attrName);\n }\n this.undoableChangeStack[p] = {\n attrName,\n oldValues,\n elements\n };\n }\n\n /**\n * This function returns a BatchCommand object which summarizes the\n * change since beginUndoableChange was called. The command can then\n * be added to the command history\n * @returns Batch command object with resulting changes\n */\n finishUndoableChange () {\n const p = this.undoChangeStackPointer--;\n const changeset = this.undoableChangeStack[p];\n const {attrName} = changeset;\n const batchCmd = new BatchCommand('Change ' + attrName);\n let i = changeset.elements.length;\n while (i--) {\n const elem = changeset.elements[i];\n if (elem == null) { continue; }\n const changes = {};\n changes[attrName] = changeset.oldValues[i];\n if (changes[attrName] !== elem.getAttribute(attrName)) {\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes, attrName));\n }\n }\n this.undoableChangeStack[p] = null;\n return batchCmd;\n }\n}\n","/**\n * Package: svedit.math\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Alexis Deveria\n * Copyright(c) 2010 Jeff Schiller\n */\n\n/**\n* @typedef AngleCoord45\n* @type {Object}\n* @property {number} x - The angle-snapped x value\n* @property {number} y - The angle-snapped y value\n* @property {number} a - The angle at which to snap\n*/\n\nimport {NS} from './svgedit.js';\nimport {getTransformList} from './svgtransformlist.js';\n\n// Constants\nconst NEAR_ZERO = 1e-14;\n\n// Throw away SVGSVGElement used for creating matrices/transforms.\nconst svg = document.createElementNS(NS.SVG, 'svg');\n\n/**\n * A (hopefully) quicker function to transform a point by a matrix\n * (this function avoids any DOM calls and just does the math)\n * @param {number} x - Float representing the x coordinate\n * @param {number} y - Float representing the y coordinate\n * @param {SVGMatrix} m - Matrix object to transform the point with\n * @returns {Object} An x, y object representing the transformed point\n*/\nexport const transformPoint = function (x, y, m) {\n return {x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f};\n};\n\n/**\n * Helper function to check if the matrix performs no actual transform\n * (i.e. exists for identity purposes)\n * @param {SVGMatrix} m - The matrix object to check\n * @returns {boolean} Indicates whether or not the matrix is 1,0,0,1,0,0\n*/\nexport const isIdentity = function (m) {\n return (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0);\n};\n\n/**\n * This function tries to return a SVGMatrix that is the multiplication m1*m2.\n * We also round to zero when it's near zero\n * @param {...SVGMatrix} args - Matrix objects to multiply\n * @returns {SVGMatrix} The matrix object resulting from the calculation\n*/\nexport const matrixMultiply = function (...args) {\n const m = args.reduceRight((prev, m1) => {\n return m1.multiply(prev);\n });\n\n if (Math.abs(m.a) < NEAR_ZERO) { m.a = 0; }\n if (Math.abs(m.b) < NEAR_ZERO) { m.b = 0; }\n if (Math.abs(m.c) < NEAR_ZERO) { m.c = 0; }\n if (Math.abs(m.d) < NEAR_ZERO) { m.d = 0; }\n if (Math.abs(m.e) < NEAR_ZERO) { m.e = 0; }\n if (Math.abs(m.f) < NEAR_ZERO) { m.f = 0; }\n\n return m;\n};\n\n/**\n * See if the given transformlist includes a non-indentity matrix transform\n * @param {Object} [tlist] - The transformlist to check\n * @returns {boolean} Whether or not a matrix transform was found\n*/\nexport const hasMatrixTransform = function (tlist) {\n if (!tlist) { return false; }\n let num = tlist.numberOfItems;\n while (num--) {\n const xform = tlist.getItem(num);\n if (xform.type === 1 && !isIdentity(xform.matrix)) { return true; }\n }\n return false;\n};\n\n/**\n * Transforms a rectangle based on the given matrix\n * @param {number} l - Float with the box's left coordinate\n * @param {number} t - Float with the box's top coordinate\n * @param {number} w - Float with the box width\n * @param {number} h - Float with the box height\n * @param {SVGMatrix} m - Matrix object to transform the box by\n * @returns {Object} An object with the following values:\n * tl - The top left coordinate (x,y object)\n * tr - The top right coordinate (x,y object)\n * bl - The bottom left coordinate (x,y object)\n * br - The bottom right coordinate (x,y object)\n * aabox - Object with the following values:\n * x - Float with the axis-aligned x coordinate\n * y - Float with the axis-aligned y coordinate\n * width - Float with the axis-aligned width coordinate\n * height - Float with the axis-aligned height coordinate\n*/\nexport const transformBox = function (l, t, w, h, m) {\n const tl = transformPoint(l, t, m),\n tr = transformPoint((l + w), t, m),\n bl = transformPoint(l, (t + h), m),\n br = transformPoint((l + w), (t + h), m),\n\n minx = Math.min(tl.x, tr.x, bl.x, br.x),\n maxx = Math.max(tl.x, tr.x, bl.x, br.x),\n miny = Math.min(tl.y, tr.y, bl.y, br.y),\n maxy = Math.max(tl.y, tr.y, bl.y, br.y);\n\n return {\n tl,\n tr,\n bl,\n br,\n aabox: {\n x: minx,\n y: miny,\n width: (maxx - minx),\n height: (maxy - miny)\n }\n };\n};\n\n/**\n * This returns a single matrix Transform for a given Transform List\n * (this is the equivalent of SVGTransformList.consolidate() but unlike\n * that method, this one does not modify the actual SVGTransformList)\n * This function is very liberal with its min, max arguments\n * @param {Object} tlist - The transformlist object\n * @param {integer} [min=0] - Optional integer indicating start transform position\n * @param {integer} [max] - Optional integer indicating end transform position;\n * defaults to one less than the tlist's numberOfItems\n * @returns {Object} A single matrix transform object\n*/\nexport const transformListToTransform = function (tlist, min, max) {\n if (tlist == null) {\n // Or should tlist = null have been prevented before this?\n return svg.createSVGTransformFromMatrix(svg.createSVGMatrix());\n }\n min = min || 0;\n max = max || (tlist.numberOfItems - 1);\n min = parseInt(min, 10);\n max = parseInt(max, 10);\n if (min > max) { const temp = max; max = min; min = temp; }\n let m = svg.createSVGMatrix();\n for (let i = min; i <= max; ++i) {\n // if our indices are out of range, just use a harmless identity matrix\n const mtom = (i >= 0 && i < tlist.numberOfItems\n ? tlist.getItem(i).matrix\n : svg.createSVGMatrix());\n m = matrixMultiply(m, mtom);\n }\n return svg.createSVGTransformFromMatrix(m);\n};\n\n/**\n * Get the matrix object for a given element\n * @param {Element} elem - The DOM element to check\n * @returns {SVGMatrix} The matrix object associated with the element's transformlist\n*/\nexport const getMatrix = function (elem) {\n const tlist = getTransformList(elem);\n return transformListToTransform(tlist).matrix;\n};\n\n/**\n * Returns a 45 degree angle coordinate associated with the two given\n * coordinates\n * @param {number} x1 - First coordinate's x value\n * @param {number} x2 - Second coordinate's x value\n * @param {number} y1 - First coordinate's y value\n * @param {number} y2 - Second coordinate's y value\n * @returns {AngleCoord45}\n*/\nexport const snapToAngle = function (x1, y1, x2, y2) {\n const snap = Math.PI / 4; // 45 degrees\n const dx = x2 - x1;\n const dy = y2 - y1;\n const angle = Math.atan2(dy, dx);\n const dist = Math.sqrt(dx * dx + dy * dy);\n const snapangle = Math.round(angle / snap) * snap;\n\n return {\n x: x1 + dist * Math.cos(snapangle),\n y: y1 + dist * Math.sin(snapangle),\n a: snapangle\n };\n};\n\n/**\n * Check if two rectangles (BBoxes objects) intersect each other\n * @param {SVGRect} r1 - The first BBox-like object\n * @param {SVGRect} r2 - The second BBox-like object\n * @returns {boolean} True if rectangles intersect\n */\nexport const rectsIntersect = function (r1, r2) {\n return r2.x < (r1.x + r1.width) &&\n (r2.x + r2.width) > r1.x &&\n r2.y < (r1.y + r1.height) &&\n (r2.y + r2.height) > r1.y;\n};\n","/* globals jQuery */\n/**\n * Package: svgedit.path\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2011 Alexis Deveria\n * Copyright(c) 2011 Jeff Schiller\n */\n\nimport './pathseg.js';\nimport {NS} from './svgedit.js';\nimport {getTransformList} from './svgtransformlist.js';\nimport {shortFloat} from './units.js';\nimport {ChangeElementCommand, BatchCommand} from './history.js';\nimport {\n transformPoint, getMatrix, snapToAngle, rectsIntersect,\n transformListToTransform\n} from './math.js';\nimport {\n assignAttributes, getElem, getRotationAngle, getBBox,\n getRefElem, findDefs, snapToGrid,\n getBBox as utilsGetBBox\n} from './svgutils.js';\nimport {\n supportsPathInsertItemBefore, supportsPathReplaceItem, isWebkit\n} from './browser.js';\n\nconst $ = jQuery;\n\nconst segData = {\n 2: ['x', 'y'],\n 4: ['x', 'y'],\n 6: ['x', 'y', 'x1', 'y1', 'x2', 'y2'],\n 8: ['x', 'y', 'x1', 'y1'],\n 10: ['x', 'y', 'r1', 'r2', 'angle', 'largeArcFlag', 'sweepFlag'],\n 12: ['x'],\n 14: ['y'],\n 16: ['x', 'y', 'x2', 'y2'],\n 18: ['x', 'y']\n};\n\nconst uiStrings = {};\nexport const setUiStrings = function (strs) {\n Object.assign(uiStrings, strs.ui);\n};\n\nlet pathFuncs = [];\n\nlet linkControlPts = true;\n\n// Stores references to paths via IDs.\n// TODO: Make this cross-document happy.\nlet pathData = {};\n\nexport const setLinkControlPoints = function (lcp) {\n linkControlPts = lcp;\n};\n\nexport let path = null;\n\nlet editorContext_ = null;\n\nexport const init = function (editorContext) {\n editorContext_ = editorContext;\n\n pathFuncs = [0, 'ClosePath'];\n const pathFuncsStrs = ['Moveto', 'Lineto', 'CurvetoCubic', 'CurvetoQuadratic', 'Arc',\n 'LinetoHorizontal', 'LinetoVertical', 'CurvetoCubicSmooth', 'CurvetoQuadraticSmooth'];\n $.each(pathFuncsStrs, function (i, s) {\n pathFuncs.push(s + 'Abs');\n pathFuncs.push(s + 'Rel');\n });\n};\n\nexport const insertItemBefore = function (elem, newseg, index) {\n // Support insertItemBefore on paths for FF2\n const list = elem.pathSegList;\n\n if (supportsPathInsertItemBefore()) {\n list.insertItemBefore(newseg, index);\n return;\n }\n const len = list.numberOfItems;\n const arr = [];\n for (let i = 0; i < len; i++) {\n const curSeg = list.getItem(i);\n arr.push(curSeg);\n }\n list.clear();\n for (let i = 0; i < len; i++) {\n if (i === index) { // index + 1\n list.appendItem(newseg);\n }\n list.appendItem(arr[i]);\n }\n};\n\n// TODO: See if this should just live in replacePathSeg\nexport const ptObjToArr = function (type, segItem) {\n const arr = segData[type], len = arr.length;\n const out = [];\n for (let i = 0; i < len; i++) {\n out[i] = segItem[arr[i]];\n }\n return out;\n};\n\nexport const getGripPt = function (seg, altPt) {\n const {path} = seg;\n let out = {\n x: altPt ? altPt.x : seg.item.x,\n y: altPt ? altPt.y : seg.item.y\n };\n\n if (path.matrix) {\n const pt = transformPoint(out.x, out.y, path.matrix);\n out = pt;\n }\n\n const currentZoom = editorContext_.getCurrentZoom();\n out.x *= currentZoom;\n out.y *= currentZoom;\n\n return out;\n};\n\nexport const getPointFromGrip = function (pt, path) {\n const out = {\n x: pt.x,\n y: pt.y\n };\n\n if (path.matrix) {\n pt = transformPoint(out.x, out.y, path.imatrix);\n out.x = pt.x;\n out.y = pt.y;\n }\n\n const currentZoom = editorContext_.getCurrentZoom();\n out.x /= currentZoom;\n out.y /= currentZoom;\n\n return out;\n};\n\n/**\n* Requires prior call to `setUiStrings` if `xlink:title`\n* to be set on the grip\n*/\nexport const addPointGrip = function (index, x, y) {\n // create the container of all the point grips\n const pointGripContainer = getGripContainer();\n\n let pointGrip = getElem('pathpointgrip_' + index);\n // create it\n if (!pointGrip) {\n pointGrip = document.createElementNS(NS.SVG, 'circle');\n const atts = {\n id: 'pathpointgrip_' + index,\n display: 'none',\n r: 4,\n fill: '#0FF',\n stroke: '#00F',\n 'stroke-width': 2,\n cursor: 'move',\n style: 'pointer-events:all'\n };\n if ('pathNodeTooltip' in uiStrings) { // May be empty if running path.js without svg-editor\n atts['xlink:title'] = uiStrings.pathNodeTooltip;\n }\n assignAttributes(pointGrip, atts);\n pointGrip = pointGripContainer.appendChild(pointGrip);\n\n const grip = $('#pathpointgrip_' + index);\n grip.dblclick(function () {\n if (path) {\n path.setSegType();\n }\n });\n }\n if (x && y) {\n // set up the point grip element and display it\n assignAttributes(pointGrip, {\n cx: x,\n cy: y,\n display: 'inline'\n });\n }\n return pointGrip;\n};\n\nexport const getGripContainer = function () {\n let c = getElem('pathpointgrip_container');\n if (!c) {\n const parent = getElem('selectorParentGroup');\n c = parent.appendChild(document.createElementNS(NS.SVG, 'g'));\n c.id = 'pathpointgrip_container';\n }\n return c;\n};\n\n/**\n* Requires prior call to `setUiStrings` if `xlink:title`\n* to be set on the grip\n*/\nexport const addCtrlGrip = function (id) {\n let pointGrip = getElem('ctrlpointgrip_' + id);\n if (pointGrip) { return pointGrip; }\n\n pointGrip = document.createElementNS(NS.SVG, 'circle');\n const atts = {\n id: 'ctrlpointgrip_' + id,\n display: 'none',\n r: 4,\n fill: '#0FF',\n stroke: '#55F',\n 'stroke-width': 1,\n cursor: 'move',\n style: 'pointer-events:all'\n };\n if ('pathCtrlPtTooltip' in uiStrings) { // May be empty if running path.js without svg-editor\n atts['xlink:title'] = uiStrings.pathCtrlPtTooltip;\n }\n assignAttributes(pointGrip, atts);\n getGripContainer().appendChild(pointGrip);\n return pointGrip;\n};\n\nexport const getCtrlLine = function (id) {\n let ctrlLine = getElem('ctrlLine_' + id);\n if (ctrlLine) { return ctrlLine; }\n\n ctrlLine = document.createElementNS(NS.SVG, 'line');\n assignAttributes(ctrlLine, {\n id: 'ctrlLine_' + id,\n stroke: '#555',\n 'stroke-width': 1,\n style: 'pointer-events:none'\n });\n getGripContainer().appendChild(ctrlLine);\n return ctrlLine;\n};\n\nexport const getPointGrip = function (seg, update) {\n const {index} = seg;\n const pointGrip = addPointGrip(index);\n\n if (update) {\n const pt = getGripPt(seg);\n assignAttributes(pointGrip, {\n cx: pt.x,\n cy: pt.y,\n display: 'inline'\n });\n }\n\n return pointGrip;\n};\n\nexport const getControlPoints = function (seg) {\n const {item, index} = seg;\n if (!('x1' in item) || !('x2' in item)) { return null; }\n const cpt = {};\n /* const pointGripContainer = */ getGripContainer();\n\n // Note that this is intentionally not seg.prev.item\n const prev = path.segs[index - 1].item;\n\n const segItems = [prev, item];\n\n for (let i = 1; i < 3; i++) {\n const id = index + 'c' + i;\n\n const ctrlLine = cpt['c' + i + '_line'] = getCtrlLine(id);\n\n const pt = getGripPt(seg, {x: item['x' + i], y: item['y' + i]});\n const gpt = getGripPt(seg, {x: segItems[i - 1].x, y: segItems[i - 1].y});\n\n assignAttributes(ctrlLine, {\n x1: pt.x,\n y1: pt.y,\n x2: gpt.x,\n y2: gpt.y,\n display: 'inline'\n });\n\n cpt['c' + i + '_line'] = ctrlLine;\n\n // create it\n const pointGrip = cpt['c' + i] = addCtrlGrip(id);\n\n assignAttributes(pointGrip, {\n cx: pt.x,\n cy: pt.y,\n display: 'inline'\n });\n cpt['c' + i] = pointGrip;\n }\n return cpt;\n};\n\n// This replaces the segment at the given index. Type is given as number.\nexport const replacePathSeg = function (type, index, pts, elem) {\n const pth = elem || path.elem;\n\n const func = 'createSVGPathSeg' + pathFuncs[type];\n const seg = pth[func].apply(pth, pts);\n\n if (supportsPathReplaceItem()) {\n pth.pathSegList.replaceItem(seg, index);\n } else {\n const segList = pth.pathSegList;\n const len = segList.numberOfItems;\n const arr = [];\n for (let i = 0; i < len; i++) {\n const curSeg = segList.getItem(i);\n arr.push(curSeg);\n }\n segList.clear();\n for (let i = 0; i < len; i++) {\n if (i === index) {\n segList.appendItem(seg);\n } else {\n segList.appendItem(arr[i]);\n }\n }\n }\n};\n\nexport const getSegSelector = function (seg, update) {\n const {index} = seg;\n let segLine = getElem('segline_' + index);\n if (!segLine) {\n const pointGripContainer = getGripContainer();\n // create segline\n segLine = document.createElementNS(NS.SVG, 'path');\n assignAttributes(segLine, {\n id: 'segline_' + index,\n display: 'none',\n fill: 'none',\n stroke: '#0FF',\n 'stroke-width': 2,\n style: 'pointer-events:none',\n d: 'M0,0 0,0'\n });\n pointGripContainer.appendChild(segLine);\n }\n\n if (update) {\n const {prev} = seg;\n if (!prev) {\n segLine.setAttribute('display', 'none');\n return segLine;\n }\n\n const pt = getGripPt(prev);\n // Set start point\n replacePathSeg(2, 0, [pt.x, pt.y], segLine);\n\n const pts = ptObjToArr(seg.type, seg.item, true);\n for (let i = 0; i < pts.length; i += 2) {\n const pt = getGripPt(seg, {x: pts[i], y: pts[i + 1]});\n pts[i] = pt.x;\n pts[i + 1] = pt.y;\n }\n\n replacePathSeg(seg.type, 1, pts, segLine);\n }\n return segLine;\n};\n\n/**\n* Takes three points and creates a smoother line based on them\n* @param ct1 - Object with x and y values (first control point)\n* @param ct2 - Object with x and y values (second control point)\n* @param pt - Object with x and y values (third point)\n* @returns Array of two \"smoothed\" point objects\n*/\nexport const smoothControlPoints = function (ct1, ct2, pt) {\n // each point must not be the origin\n const x1 = ct1.x - pt.x,\n y1 = ct1.y - pt.y,\n x2 = ct2.x - pt.x,\n y2 = ct2.y - pt.y;\n\n if ((x1 !== 0 || y1 !== 0) && (x2 !== 0 || y2 !== 0)) {\n const\n r1 = Math.sqrt(x1 * x1 + y1 * y1),\n r2 = Math.sqrt(x2 * x2 + y2 * y2),\n nct1 = editorContext_.getSVGRoot().createSVGPoint(),\n nct2 = editorContext_.getSVGRoot().createSVGPoint();\n let anglea = Math.atan2(y1, x1),\n angleb = Math.atan2(y2, x2);\n if (anglea < 0) { anglea += 2 * Math.PI; }\n if (angleb < 0) { angleb += 2 * Math.PI; }\n\n const angleBetween = Math.abs(anglea - angleb),\n angleDiff = Math.abs(Math.PI - angleBetween) / 2;\n\n let newAnglea, newAngleb;\n if (anglea - angleb > 0) {\n newAnglea = angleBetween < Math.PI ? (anglea + angleDiff) : (anglea - angleDiff);\n newAngleb = angleBetween < Math.PI ? (angleb - angleDiff) : (angleb + angleDiff);\n } else {\n newAnglea = angleBetween < Math.PI ? (anglea - angleDiff) : (anglea + angleDiff);\n newAngleb = angleBetween < Math.PI ? (angleb + angleDiff) : (angleb - angleDiff);\n }\n\n // rotate the points\n nct1.x = r1 * Math.cos(newAnglea) + pt.x;\n nct1.y = r1 * Math.sin(newAnglea) + pt.y;\n nct2.x = r2 * Math.cos(newAngleb) + pt.x;\n nct2.y = r2 * Math.sin(newAngleb) + pt.y;\n\n return [nct1, nct2];\n }\n return undefined;\n};\n\nexport class Segment {\n constructor (index, item) {\n this.selected = false;\n this.index = index;\n this.item = item;\n this.type = item.pathSegType;\n\n this.ctrlpts = [];\n this.ptgrip = null;\n this.segsel = null;\n }\n\n showCtrlPts (y) {\n for (const i in this.ctrlpts) {\n if (this.ctrlpts.hasOwnProperty(i)) {\n this.ctrlpts[i].setAttribute('display', y ? 'inline' : 'none');\n }\n }\n }\n\n selectCtrls (y) {\n $('#ctrlpointgrip_' + this.index + 'c1, #ctrlpointgrip_' + this.index + 'c2')\n .attr('fill', y ? '#0FF' : '#EEE');\n }\n\n show (y) {\n if (this.ptgrip) {\n this.ptgrip.setAttribute('display', y ? 'inline' : 'none');\n this.segsel.setAttribute('display', y ? 'inline' : 'none');\n // Show/hide all control points if available\n this.showCtrlPts(y);\n }\n }\n\n select (y) {\n if (this.ptgrip) {\n this.ptgrip.setAttribute('stroke', y ? '#0FF' : '#00F');\n this.segsel.setAttribute('display', y ? 'inline' : 'none');\n if (this.ctrlpts) {\n this.selectCtrls(y);\n }\n this.selected = y;\n }\n }\n\n addGrip () {\n this.ptgrip = getPointGrip(this, true);\n this.ctrlpts = getControlPoints(this, true);\n this.segsel = getSegSelector(this, true);\n }\n\n update (full) {\n if (this.ptgrip) {\n const pt = getGripPt(this);\n assignAttributes(this.ptgrip, {\n cx: pt.x,\n cy: pt.y\n });\n\n getSegSelector(this, true);\n\n if (this.ctrlpts) {\n if (full) {\n this.item = path.elem.pathSegList.getItem(this.index);\n this.type = this.item.pathSegType;\n }\n getControlPoints(this);\n }\n // this.segsel.setAttribute('display', y ? 'inline' : 'none');\n }\n }\n\n move (dx, dy) {\n const {item} = this;\n\n const curPts = this.ctrlpts\n ? [item.x += dx, item.y += dy,\n item.x1, item.y1, item.x2 += dx, item.y2 += dy\n ]\n : [item.x += dx, item.y += dy];\n\n replacePathSeg(this.type, this.index, curPts);\n\n if (this.next && this.next.ctrlpts) {\n const next = this.next.item;\n const nextPts = [next.x, next.y,\n next.x1 += dx, next.y1 += dy, next.x2, next.y2];\n replacePathSeg(this.next.type, this.next.index, nextPts);\n }\n\n if (this.mate) {\n // The last point of a closed subpath has a 'mate',\n // which is the 'M' segment of the subpath\n const {item} = this.mate;\n const pts = [item.x += dx, item.y += dy];\n replacePathSeg(this.mate.type, this.mate.index, pts);\n // Has no grip, so does not need 'updating'?\n }\n\n this.update(true);\n if (this.next) { this.next.update(true); }\n }\n\n setLinked (num) {\n let seg, anum, pt;\n if (num === 2) {\n anum = 1;\n seg = this.next;\n if (!seg) { return; }\n pt = this.item;\n } else {\n anum = 2;\n seg = this.prev;\n if (!seg) { return; }\n pt = seg.item;\n }\n\n const {item} = seg;\n item['x' + anum] = pt.x + (pt.x - this.item['x' + num]);\n item['y' + anum] = pt.y + (pt.y - this.item['y' + num]);\n\n const pts = [item.x, item.y,\n item.x1, item.y1,\n item.x2, item.y2];\n\n replacePathSeg(seg.type, seg.index, pts);\n seg.update(true);\n }\n\n moveCtrl (num, dx, dy) {\n const {item} = this;\n item['x' + num] += dx;\n item['y' + num] += dy;\n\n const pts = [item.x, item.y,\n item.x1, item.y1, item.x2, item.y2];\n\n replacePathSeg(this.type, this.index, pts);\n this.update(true);\n }\n\n setType (newType, pts) {\n replacePathSeg(newType, this.index, pts);\n this.type = newType;\n this.item = path.elem.pathSegList.getItem(this.index);\n this.showCtrlPts(newType === 6);\n this.ctrlpts = getControlPoints(this);\n this.update(true);\n }\n}\n\nexport class Path {\n constructor (elem) {\n if (!elem || elem.tagName !== 'path') {\n throw new Error('svgedit.path.Path constructed without a element');\n }\n\n this.elem = elem;\n this.segs = [];\n this.selected_pts = [];\n path = this;\n\n this.init();\n }\n\n // Reset path data\n init () {\n // Hide all grips, etc\n\n // fixed, needed to work on all found elements, not just first\n $(getGripContainer()).find('*').each(function () {\n $(this).attr('display', 'none');\n });\n\n const segList = this.elem.pathSegList;\n const len = segList.numberOfItems;\n this.segs = [];\n this.selected_pts = [];\n this.first_seg = null;\n\n // Set up segs array\n for (let i = 0; i < len; i++) {\n const item = segList.getItem(i);\n const segment = new Segment(i, item);\n segment.path = this;\n this.segs.push(segment);\n }\n\n const {segs} = this;\n\n let startI = null;\n for (let i = 0; i < len; i++) {\n const seg = segs[i];\n const nextSeg = (i + 1) >= len ? null : segs[i + 1];\n const prevSeg = (i - 1) < 0 ? null : segs[i - 1];\n if (seg.type === 2) {\n if (prevSeg && prevSeg.type !== 1) {\n // New sub-path, last one is open,\n // so add a grip to last sub-path's first point\n const startSeg = segs[startI];\n startSeg.next = segs[startI + 1];\n startSeg.next.prev = startSeg;\n startSeg.addGrip();\n }\n // Remember that this is a starter seg\n startI = i;\n } else if (nextSeg && nextSeg.type === 1) {\n // This is the last real segment of a closed sub-path\n // Next is first seg after \"M\"\n seg.next = segs[startI + 1];\n\n // First seg after \"M\"'s prev is this\n seg.next.prev = seg;\n seg.mate = segs[startI];\n seg.addGrip();\n if (this.first_seg == null) {\n this.first_seg = seg;\n }\n } else if (!nextSeg) {\n if (seg.type !== 1) {\n // Last seg, doesn't close so add a grip\n // to last sub-path's first point\n const startSeg = segs[startI];\n startSeg.next = segs[startI + 1];\n startSeg.next.prev = startSeg;\n startSeg.addGrip();\n seg.addGrip();\n\n if (!this.first_seg) {\n // Open path, so set first as real first and add grip\n this.first_seg = segs[startI];\n }\n }\n } else if (seg.type !== 1) {\n // Regular segment, so add grip and its \"next\"\n seg.addGrip();\n\n // Don't set its \"next\" if it's an \"M\"\n if (nextSeg && nextSeg.type !== 2) {\n seg.next = nextSeg;\n seg.next.prev = seg;\n }\n }\n }\n return this;\n }\n\n eachSeg (fn) {\n const len = this.segs.length;\n for (let i = 0; i < len; i++) {\n const ret = fn.call(this.segs[i], i);\n if (ret === false) { break; }\n }\n }\n\n addSeg (index) {\n // Adds a new segment\n const seg = this.segs[index];\n if (!seg.prev) { return; }\n\n const {prev} = seg;\n let newseg, newX, newY;\n switch (seg.item.pathSegType) {\n case 4: {\n newX = (seg.item.x + prev.item.x) / 2;\n newY = (seg.item.y + prev.item.y) / 2;\n newseg = this.elem.createSVGPathSegLinetoAbs(newX, newY);\n break;\n } case 6: { // make it a curved segment to preserve the shape (WRS)\n // https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm#Geometric_interpretation\n const p0x = (prev.item.x + seg.item.x1) / 2;\n const p1x = (seg.item.x1 + seg.item.x2) / 2;\n const p2x = (seg.item.x2 + seg.item.x) / 2;\n const p01x = (p0x + p1x) / 2;\n const p12x = (p1x + p2x) / 2;\n newX = (p01x + p12x) / 2;\n const p0y = (prev.item.y + seg.item.y1) / 2;\n const p1y = (seg.item.y1 + seg.item.y2) / 2;\n const p2y = (seg.item.y2 + seg.item.y) / 2;\n const p01y = (p0y + p1y) / 2;\n const p12y = (p1y + p2y) / 2;\n newY = (p01y + p12y) / 2;\n newseg = this.elem.createSVGPathSegCurvetoCubicAbs(newX, newY, p0x, p0y, p01x, p01y);\n const pts = [seg.item.x, seg.item.y, p12x, p12y, p2x, p2y];\n replacePathSeg(seg.type, index, pts);\n break;\n }\n }\n\n insertItemBefore(this.elem, newseg, index);\n }\n\n deleteSeg (index) {\n const seg = this.segs[index];\n const list = this.elem.pathSegList;\n\n seg.show(false);\n const {next} = seg;\n if (seg.mate) {\n // Make the next point be the \"M\" point\n const pt = [next.item.x, next.item.y];\n replacePathSeg(2, next.index, pt);\n\n // Reposition last node\n replacePathSeg(4, seg.index, pt);\n\n list.removeItem(seg.mate.index);\n } else if (!seg.prev) {\n // First node of open path, make next point the M\n // const {item} = seg;\n const pt = [next.item.x, next.item.y];\n replacePathSeg(2, seg.next.index, pt);\n list.removeItem(index);\n } else {\n list.removeItem(index);\n }\n }\n\n subpathIsClosed (index) {\n let closed = false;\n // Check if subpath is already open\n path.eachSeg(function (i) {\n if (i <= index) { return true; }\n if (this.type === 2) {\n // Found M first, so open\n return false;\n }\n if (this.type === 1) {\n // Found Z first, so closed\n closed = true;\n return false;\n }\n });\n\n return closed;\n }\n\n removePtFromSelection (index) {\n const pos = this.selected_pts.indexOf(index);\n if (pos === -1) {\n return;\n }\n this.segs[index].select(false);\n this.selected_pts.splice(pos, 1);\n }\n\n clearSelection () {\n this.eachSeg(function () {\n // 'this' is the segment here\n this.select(false);\n });\n this.selected_pts = [];\n }\n\n storeD () {\n this.last_d = this.elem.getAttribute('d');\n }\n\n show (y) {\n // Shows this path's segment grips\n this.eachSeg(function () {\n // 'this' is the segment here\n this.show(y);\n });\n if (y) {\n this.selectPt(this.first_seg.index);\n }\n return this;\n }\n\n // Move selected points\n movePts (dx, dy) {\n let i = this.selected_pts.length;\n while (i--) {\n const seg = this.segs[this.selected_pts[i]];\n seg.move(dx, dy);\n }\n }\n\n moveCtrl (dx, dy) {\n const seg = this.segs[this.selected_pts[0]];\n seg.moveCtrl(this.dragctrl, dx, dy);\n if (linkControlPts) {\n seg.setLinked(this.dragctrl);\n }\n }\n\n setSegType (newType) {\n this.storeD();\n let i = this.selected_pts.length;\n let text;\n while (i--) {\n const selPt = this.selected_pts[i];\n\n // Selected seg\n const cur = this.segs[selPt];\n const {prev} = cur;\n if (!prev) { continue; }\n\n if (!newType) { // double-click, so just toggle\n text = 'Toggle Path Segment Type';\n\n // Toggle segment to curve/straight line\n const oldType = cur.type;\n\n newType = (oldType === 6) ? 4 : 6;\n }\n\n newType = Number(newType);\n\n const curX = cur.item.x;\n const curY = cur.item.y;\n const prevX = prev.item.x;\n const prevY = prev.item.y;\n let points;\n switch (newType) {\n case 6: {\n if (cur.olditem) {\n const old = cur.olditem;\n points = [curX, curY, old.x1, old.y1, old.x2, old.y2];\n } else {\n const diffX = curX - prevX;\n const diffY = curY - prevY;\n // get control points from straight line segment\n /*\n const ct1x = (prevX + (diffY/2));\n const ct1y = (prevY - (diffX/2));\n const ct2x = (curX + (diffY/2));\n const ct2y = (curY - (diffX/2));\n */\n // create control points on the line to preserve the shape (WRS)\n const ct1x = (prevX + (diffX / 3));\n const ct1y = (prevY + (diffY / 3));\n const ct2x = (curX - (diffX / 3));\n const ct2y = (curY - (diffY / 3));\n points = [curX, curY, ct1x, ct1y, ct2x, ct2y];\n }\n break;\n } case 4: {\n points = [curX, curY];\n\n // Store original prevve segment nums\n cur.olditem = cur.item;\n break;\n }\n }\n\n cur.setType(newType, points);\n }\n path.endChanges(text);\n }\n\n selectPt (pt, ctrlNum) {\n this.clearSelection();\n if (pt == null) {\n this.eachSeg(function (i) {\n // 'this' is the segment here.\n if (this.prev) {\n pt = i;\n }\n });\n }\n this.addPtsToSelection(pt);\n if (ctrlNum) {\n this.dragctrl = ctrlNum;\n\n if (linkControlPts) {\n this.segs[pt].setLinked(ctrlNum);\n }\n }\n }\n\n // Update position of all points\n update () {\n const {elem} = this;\n if (getRotationAngle(elem)) {\n this.matrix = getMatrix(elem);\n this.imatrix = this.matrix.inverse();\n } else {\n this.matrix = null;\n this.imatrix = null;\n }\n\n this.eachSeg(function (i) {\n this.item = elem.pathSegList.getItem(i);\n this.update();\n });\n\n return this;\n }\n\n endChanges (text) {\n if (isWebkit()) { editorContext_.resetD(this.elem); }\n const cmd = new ChangeElementCommand(this.elem, {d: this.last_d}, text);\n editorContext_.endChanges({cmd, elem: this.elem});\n }\n\n addPtsToSelection (indexes) {\n if (!Array.isArray(indexes)) { indexes = [indexes]; }\n for (let i = 0; i < indexes.length; i++) {\n const index = indexes[i];\n const seg = this.segs[index];\n if (seg.ptgrip) {\n if (!this.selected_pts.includes(index) && index >= 0) {\n this.selected_pts.push(index);\n }\n }\n }\n this.selected_pts.sort();\n let i = this.selected_pts.length;\n const grips = [];\n grips.length = i;\n // Loop through points to be selected and highlight each\n while (i--) {\n const pt = this.selected_pts[i];\n const seg = this.segs[pt];\n seg.select(true);\n grips[i] = seg.ptgrip;\n }\n\n const closedSubpath = this.subpathIsClosed(this.selected_pts[0]);\n editorContext_.addPtsToSelection({grips, closedSubpath});\n }\n}\n\nexport const getPath_ = function (elem) {\n let p = pathData[elem.id];\n if (!p) {\n p = pathData[elem.id] = new Path(elem);\n }\n return p;\n};\n\nexport const removePath_ = function (id) {\n if (id in pathData) { delete pathData[id]; }\n};\n\nlet newcx, newcy, oldcx, oldcy, angle;\n\nconst getRotVals = function (x, y) {\n let dx = x - oldcx;\n let dy = y - oldcy;\n\n // rotate the point around the old center\n let r = Math.sqrt(dx * dx + dy * dy);\n let theta = Math.atan2(dy, dx) + angle;\n dx = r * Math.cos(theta) + oldcx;\n dy = r * Math.sin(theta) + oldcy;\n\n // dx,dy should now hold the actual coordinates of each\n // point after being rotated\n\n // now we want to rotate them around the new center in the reverse direction\n dx -= newcx;\n dy -= newcy;\n\n r = Math.sqrt(dx * dx + dy * dy);\n theta = Math.atan2(dy, dx) - angle;\n\n return {x: r * Math.cos(theta) + newcx,\n y: r * Math.sin(theta) + newcy};\n};\n\n// If the path was rotated, we must now pay the piper:\n// Every path point must be rotated into the rotated coordinate system of\n// its old center, then determine the new center, then rotate it back\n// This is because we want the path to remember its rotation\n\n// TODO: This is still using ye olde transform methods, can probably\n// be optimized or even taken care of by `recalculateDimensions`\nexport const recalcRotatedPath = function () {\n const currentPath = path.elem;\n angle = getRotationAngle(currentPath, true);\n if (!angle) { return; }\n // selectedBBoxes[0] = path.oldbbox;\n const oldbox = path.oldbbox; // selectedBBoxes[0],\n oldcx = oldbox.x + oldbox.width / 2;\n oldcy = oldbox.y + oldbox.height / 2;\n let box = getBBox(currentPath);\n newcx = box.x + box.width / 2;\n newcy = box.y + box.height / 2;\n\n // un-rotate the new center to the proper position\n const dx = newcx - oldcx,\n dy = newcy - oldcy,\n r = Math.sqrt(dx * dx + dy * dy),\n theta = Math.atan2(dy, dx) + angle;\n\n newcx = r * Math.cos(theta) + oldcx;\n newcy = r * Math.sin(theta) + oldcy;\n\n const list = currentPath.pathSegList;\n\n let i = list.numberOfItems;\n while (i) {\n i -= 1;\n const seg = list.getItem(i),\n type = seg.pathSegType;\n if (type === 1) { continue; }\n\n const rvals = getRotVals(seg.x, seg.y),\n points = [rvals.x, rvals.y];\n if (seg.x1 != null && seg.x2 != null) {\n const cVals1 = getRotVals(seg.x1, seg.y1);\n const cVals2 = getRotVals(seg.x2, seg.y2);\n points.splice(points.length, 0, cVals1.x, cVals1.y, cVals2.x, cVals2.y);\n }\n replacePathSeg(type, i, points);\n } // loop for each point\n\n box = getBBox(currentPath);\n // selectedBBoxes[0].x = box.x; selectedBBoxes[0].y = box.y;\n // selectedBBoxes[0].width = box.width; selectedBBoxes[0].height = box.height;\n\n // now we must set the new transform to be rotated around the new center\n const Rnc = editorContext_.getSVGRoot().createSVGTransform(),\n tlist = getTransformList(currentPath);\n Rnc.setRotate((angle * 180.0 / Math.PI), newcx, newcy);\n tlist.replaceItem(Rnc, 0);\n};\n\n// ====================================\n// Public API starts here\n\nexport const clearData = function () {\n pathData = {};\n};\n\n// Making public for mocking\nexport const reorientGrads = function (elem, m) {\n const bb = utilsGetBBox(elem);\n for (let i = 0; i < 2; i++) {\n const type = i === 0 ? 'fill' : 'stroke';\n const attrVal = elem.getAttribute(type);\n if (attrVal && attrVal.startsWith('url(')) {\n const grad = getRefElem(attrVal);\n if (grad.tagName === 'linearGradient') {\n let x1 = grad.getAttribute('x1') || 0;\n let y1 = grad.getAttribute('y1') || 0;\n let x2 = grad.getAttribute('x2') || 1;\n let y2 = grad.getAttribute('y2') || 0;\n\n // Convert to USOU points\n x1 = (bb.width * x1) + bb.x;\n y1 = (bb.height * y1) + bb.y;\n x2 = (bb.width * x2) + bb.x;\n y2 = (bb.height * y2) + bb.y;\n\n // Transform those points\n const pt1 = transformPoint(x1, y1, m);\n const pt2 = transformPoint(x2, y2, m);\n\n // Convert back to BB points\n const gCoords = {};\n\n gCoords.x1 = (pt1.x - bb.x) / bb.width;\n gCoords.y1 = (pt1.y - bb.y) / bb.height;\n gCoords.x2 = (pt2.x - bb.x) / bb.width;\n gCoords.y2 = (pt2.y - bb.y) / bb.height;\n\n const newgrad = grad.cloneNode(true);\n $(newgrad).attr(gCoords);\n\n newgrad.id = editorContext_.getNextId();\n findDefs().appendChild(newgrad);\n elem.setAttribute(type, 'url(#' + newgrad.id + ')');\n }\n }\n }\n};\n\n// this is how we map paths to our preferred relative segment types\nconst pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',\n 'H', 'h', 'V', 'v', 'S', 's', 'T', 't'];\n\n/**\n * TODO: move to pathActions.js when migrating rest of pathActions out of svgcanvas.js\n * Convert a path to one with only absolute or relative values\n * @param {Object} path - the path to convert\n * @param {boolean} toRel - true of convert to relative\n * @returns {string}\n */\nexport const convertPath = function (path, toRel) {\n const segList = path.pathSegList;\n const len = segList.numberOfItems;\n let curx = 0, cury = 0;\n let d = '';\n let lastM = null;\n\n for (let i = 0; i < len; ++i) {\n const seg = segList.getItem(i);\n // if these properties are not in the segment, set them to zero\n let x = seg.x || 0,\n y = seg.y || 0,\n x1 = seg.x1 || 0,\n y1 = seg.y1 || 0,\n x2 = seg.x2 || 0,\n y2 = seg.y2 || 0;\n\n const type = seg.pathSegType;\n let letter = pathMap[type]['to' + (toRel ? 'Lower' : 'Upper') + 'Case']();\n\n switch (type) {\n case 1: // z,Z closepath (Z/z)\n d += 'z';\n if (lastM && !toRel) {\n curx = lastM[0];\n cury = lastM[1];\n }\n break;\n case 12: // absolute horizontal line (H)\n x -= curx;\n // Fallthrough\n case 13: // relative horizontal line (h)\n if (toRel) {\n curx += x;\n letter = 'l';\n } else {\n x += curx;\n curx = x;\n letter = 'L';\n }\n // Convert to \"line\" for easier editing\n d += pathDSegment(letter, [[x, cury]]);\n break;\n case 14: // absolute vertical line (V)\n y -= cury;\n // Fallthrough\n case 15: // relative vertical line (v)\n if (toRel) {\n cury += y;\n letter = 'l';\n } else {\n y += cury;\n cury = y;\n letter = 'L';\n }\n // Convert to \"line\" for easier editing\n d += pathDSegment(letter, [[curx, y]]);\n break;\n case 2: // absolute move (M)\n case 4: // absolute line (L)\n case 18: // absolute smooth quad (T)\n x -= curx;\n y -= cury;\n // Fallthrough\n case 5: // relative line (l)\n case 3: // relative move (m)\n case 19: // relative smooth quad (t)\n if (toRel) {\n curx += x;\n cury += y;\n } else {\n x += curx;\n y += cury;\n curx = x;\n cury = y;\n }\n if (type === 2 || type === 3) { lastM = [curx, cury]; }\n\n d += pathDSegment(letter, [[x, y]]);\n break;\n case 6: // absolute cubic (C)\n x -= curx; x1 -= curx; x2 -= curx;\n y -= cury; y1 -= cury; y2 -= cury;\n // Fallthrough\n case 7: // relative cubic (c)\n if (toRel) {\n curx += x;\n cury += y;\n } else {\n x += curx; x1 += curx; x2 += curx;\n y += cury; y1 += cury; y2 += cury;\n curx = x;\n cury = y;\n }\n d += pathDSegment(letter, [[x1, y1], [x2, y2], [x, y]]);\n break;\n case 8: // absolute quad (Q)\n x -= curx; x1 -= curx;\n y -= cury; y1 -= cury;\n // Fallthrough\n case 9: // relative quad (q)\n if (toRel) {\n curx += x;\n cury += y;\n } else {\n x += curx; x1 += curx;\n y += cury; y1 += cury;\n curx = x;\n cury = y;\n }\n d += pathDSegment(letter, [[x1, y1], [x, y]]);\n break;\n case 10: // absolute elliptical arc (A)\n x -= curx;\n y -= cury;\n // Fallthrough\n case 11: // relative elliptical arc (a)\n if (toRel) {\n curx += x;\n cury += y;\n } else {\n x += curx;\n y += cury;\n curx = x;\n cury = y;\n }\n d += pathDSegment(letter, [[seg.r1, seg.r2]], [\n seg.angle,\n (seg.largeArcFlag ? 1 : 0),\n (seg.sweepFlag ? 1 : 0)\n ], [x, y]);\n break;\n case 16: // absolute smooth cubic (S)\n x -= curx; x2 -= curx;\n y -= cury; y2 -= cury;\n // Fallthrough\n case 17: // relative smooth cubic (s)\n if (toRel) {\n curx += x;\n cury += y;\n } else {\n x += curx; x2 += curx;\n y += cury; y2 += cury;\n curx = x;\n cury = y;\n }\n d += pathDSegment(letter, [[x2, y2], [x, y]]);\n break;\n } // switch on path segment type\n } // for each segment\n return d;\n};\n\n/**\n * TODO: refactor callers in convertPath to use getPathDFromSegments instead of this function.\n * Legacy code refactored from svgcanvas.pathActions.convertPath\n * @param letter - path segment command\n * @param {Array.>} points - x,y points.\n * @param {Array.>=} morePoints - x,y points\n * @param {Array.=}lastPoint - x,y point\n * @returns {string}\n */\nfunction pathDSegment (letter, points, morePoints, lastPoint) {\n $.each(points, function (i, pnt) {\n points[i] = shortFloat(pnt);\n });\n let segment = letter + points.join(' ');\n if (morePoints) {\n segment += ' ' + morePoints.join(' ');\n }\n if (lastPoint) {\n segment += ' ' + shortFloat(lastPoint);\n }\n return segment;\n}\n\n/**\n* Group: Path edit functions\n* Functions relating to editing path elements\n*/\nexport const pathActions = (function () {\n let subpath = false;\n let newPoint, firstCtrl;\n\n let currentPath = null;\n let hasMoved = false;\n // No `editorContext_` yet but should be ok as is `null` by default\n // editorContext_.setDrawnPath(null);\n\n // This function converts a polyline (created by the fh_path tool) into\n // a path element and coverts every three line segments into a single bezier\n // curve in an attempt to smooth out the free-hand\n const smoothPolylineIntoPath = function (element) {\n let i;\n const {points} = element;\n const N = points.numberOfItems;\n if (N >= 4) {\n // loop through every 3 points and convert to a cubic bezier curve segment\n //\n // NOTE: this is cheating, it means that every 3 points has the potential to\n // be a corner instead of treating each point in an equal manner. In general,\n // this technique does not look that good.\n //\n // I am open to better ideas!\n //\n // Reading:\n // - http://www.efg2.com/Lab/Graphics/Jean-YvesQueinecBezierCurves.htm\n // - https://www.codeproject.com/KB/graphics/BezierSpline.aspx?msg=2956963\n // - https://www.ian-ko.com/ET_GeoWizards/UserGuide/smooth.htm\n // - https://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html\n let curpos = points.getItem(0), prevCtlPt = null;\n let d = [];\n d.push(['M', curpos.x, ',', curpos.y, ' C'].join(''));\n for (i = 1; i <= (N - 4); i += 3) {\n let ct1 = points.getItem(i);\n const ct2 = points.getItem(i + 1);\n const end = points.getItem(i + 2);\n\n // if the previous segment had a control point, we want to smooth out\n // the control points on both sides\n if (prevCtlPt) {\n const newpts = smoothControlPoints(prevCtlPt, ct1, curpos);\n if (newpts && newpts.length === 2) {\n const prevArr = d[d.length - 1].split(',');\n prevArr[2] = newpts[0].x;\n prevArr[3] = newpts[0].y;\n d[d.length - 1] = prevArr.join(',');\n ct1 = newpts[1];\n }\n }\n\n d.push([ct1.x, ct1.y, ct2.x, ct2.y, end.x, end.y].join(','));\n\n curpos = end;\n prevCtlPt = ct2;\n }\n // handle remaining line segments\n d.push('L');\n while (i < N) {\n const pt = points.getItem(i);\n d.push([pt.x, pt.y].join(','));\n i++;\n }\n d = d.join(' ');\n\n // create new path element\n element = editorContext_.addSvgElementFromJson({\n element: 'path',\n curStyles: true,\n attr: {\n id: editorContext_.getId(),\n d,\n fill: 'none'\n }\n });\n // No need to call \"changed\", as this is already done under mouseUp\n }\n return element;\n };\n\n return {\n mouseDown (evt, mouseTarget, startX, startY) {\n let id;\n if (editorContext_.getCurrentMode() === 'path') {\n let mouseX = startX; // Was this meant to work with the other `mouseX`? (was defined globally so adding `let` to at least avoid a global)\n let mouseY = startY; // Was this meant to work with the other `mouseY`? (was defined globally so adding `let` to at least avoid a global)\n\n const currentZoom = editorContext_.getCurrentZoom();\n let x = mouseX / currentZoom,\n y = mouseY / currentZoom,\n stretchy = getElem('path_stretch_line');\n newPoint = [x, y];\n\n if (editorContext_.getGridSnapping()) {\n x = snapToGrid(x);\n y = snapToGrid(y);\n mouseX = snapToGrid(mouseX);\n mouseY = snapToGrid(mouseY);\n }\n\n if (!stretchy) {\n stretchy = document.createElementNS(NS.SVG, 'path');\n assignAttributes(stretchy, {\n id: 'path_stretch_line',\n stroke: '#22C',\n 'stroke-width': '0.5',\n fill: 'none'\n });\n stretchy = getElem('selectorParentGroup').appendChild(stretchy);\n }\n stretchy.setAttribute('display', 'inline');\n\n let keep = null;\n let index;\n // if pts array is empty, create path element with M at current point\n let drawnPath = editorContext_.getDrawnPath();\n if (!drawnPath) {\n const dAttr = 'M' + x + ',' + y + ' '; // Was this meant to work with the other `dAttr`? (was defined globally so adding `var` to at least avoid a global)\n drawnPath = editorContext_.setDrawnPath(editorContext_.addSvgElementFromJson({\n element: 'path',\n curStyles: true,\n attr: {\n d: dAttr,\n id: editorContext_.getNextId(),\n opacity: editorContext_.getOpacity() / 2\n }\n }));\n // set stretchy line to first point\n stretchy.setAttribute('d', ['M', mouseX, mouseY, mouseX, mouseY].join(' '));\n index = subpath ? path.segs.length : 0;\n addPointGrip(index, mouseX, mouseY);\n } else {\n // determine if we clicked on an existing point\n const seglist = drawnPath.pathSegList;\n let i = seglist.numberOfItems;\n const FUZZ = 6 / currentZoom;\n let clickOnPoint = false;\n while (i) {\n i--;\n const item = seglist.getItem(i);\n const px = item.x, py = item.y;\n // found a matching point\n if (x >= (px - FUZZ) && x <= (px + FUZZ) &&\n y >= (py - FUZZ) && y <= (py + FUZZ)\n ) {\n clickOnPoint = true;\n break;\n }\n }\n\n // get path element that we are in the process of creating\n id = editorContext_.getId();\n\n // Remove previous path object if previously created\n removePath_(id);\n\n const newpath = getElem(id);\n let newseg;\n let sSeg;\n const len = seglist.numberOfItems;\n // if we clicked on an existing point, then we are done this path, commit it\n // (i, i+1) are the x,y that were clicked on\n if (clickOnPoint) {\n // if clicked on any other point but the first OR\n // the first point was clicked on and there are less than 3 points\n // then leave the path open\n // otherwise, close the path\n if (i <= 1 && len >= 2) {\n // Create end segment\n const absX = seglist.getItem(0).x;\n const absY = seglist.getItem(0).y;\n\n sSeg = stretchy.pathSegList.getItem(1);\n if (sSeg.pathSegType === 4) {\n newseg = drawnPath.createSVGPathSegLinetoAbs(absX, absY);\n } else {\n newseg = drawnPath.createSVGPathSegCurvetoCubicAbs(\n absX,\n absY,\n sSeg.x1 / currentZoom,\n sSeg.y1 / currentZoom,\n absX,\n absY\n );\n }\n\n const endseg = drawnPath.createSVGPathSegClosePath();\n seglist.appendItem(newseg);\n seglist.appendItem(endseg);\n } else if (len < 3) {\n keep = false;\n return keep;\n }\n $(stretchy).remove();\n\n // This will signal to commit the path\n // const element = newpath; // Other event handlers define own `element`, so this was probably not meant to interact with them or one which shares state (as there were none); I therefore adding a missing `var` to avoid a global\n drawnPath = editorContext_.setDrawnPath(null);\n editorContext_.setStarted(false);\n\n if (subpath) {\n if (path.matrix) {\n editorContext_.remapElement(newpath, {}, path.matrix.inverse());\n }\n\n const newD = newpath.getAttribute('d');\n const origD = $(path.elem).attr('d');\n $(path.elem).attr('d', origD + newD);\n $(newpath).remove();\n if (path.matrix) {\n recalcRotatedPath();\n }\n init();\n pathActions.toEditMode(path.elem);\n path.selectPt();\n return false;\n }\n // else, create a new point, update path element\n } else {\n // Checks if current target or parents are #svgcontent\n if (!$.contains(\n editorContext_.getContainer(),\n editorContext_.getMouseTarget(evt)\n )) {\n // Clicked outside canvas, so don't make point\n console.log('Clicked outside canvas');\n return false;\n }\n\n const num = drawnPath.pathSegList.numberOfItems;\n const last = drawnPath.pathSegList.getItem(num - 1);\n const lastx = last.x, lasty = last.y;\n\n if (evt.shiftKey) {\n const xya = snapToAngle(lastx, lasty, x, y);\n ({x, y} = xya);\n }\n\n // Use the segment defined by stretchy\n sSeg = stretchy.pathSegList.getItem(1);\n if (sSeg.pathSegType === 4) {\n newseg = drawnPath.createSVGPathSegLinetoAbs(\n editorContext_.round(x),\n editorContext_.round(y)\n );\n } else {\n newseg = drawnPath.createSVGPathSegCurvetoCubicAbs(\n editorContext_.round(x),\n editorContext_.round(y),\n sSeg.x1 / currentZoom,\n sSeg.y1 / currentZoom,\n sSeg.x2 / currentZoom,\n sSeg.y2 / currentZoom\n );\n }\n\n drawnPath.pathSegList.appendItem(newseg);\n\n x *= currentZoom;\n y *= currentZoom;\n\n // set stretchy line to latest point\n stretchy.setAttribute('d', ['M', x, y, x, y].join(' '));\n index = num;\n if (subpath) { index += path.segs.length; }\n addPointGrip(index, x, y);\n }\n // keep = true;\n }\n\n return;\n }\n\n // TODO: Make sure currentPath isn't null at this point\n if (!path) { return; }\n\n path.storeD();\n\n ({id} = evt.target);\n let curPt;\n if (id.substr(0, 14) === 'pathpointgrip_') {\n // Select this point\n curPt = path.cur_pt = parseInt(id.substr(14), 10);\n path.dragging = [startX, startY];\n const seg = path.segs[curPt];\n\n // only clear selection if shift is not pressed (otherwise, add\n // node to selection)\n if (!evt.shiftKey) {\n if (path.selected_pts.length <= 1 || !seg.selected) {\n path.clearSelection();\n }\n path.addPtsToSelection(curPt);\n } else if (seg.selected) {\n path.removePtFromSelection(curPt);\n } else {\n path.addPtsToSelection(curPt);\n }\n } else if (id.startsWith('ctrlpointgrip_')) {\n path.dragging = [startX, startY];\n\n const parts = id.split('_')[1].split('c');\n curPt = Number(parts[0]);\n const ctrlNum = Number(parts[1]);\n path.selectPt(curPt, ctrlNum);\n }\n\n // Start selection box\n if (!path.dragging) {\n let rubberBox = editorContext_.getRubberBox();\n if (rubberBox == null) {\n rubberBox = editorContext_.setRubberBox(\n editorContext_.selectorManager.getRubberBandBox()\n );\n }\n const currentZoom = editorContext_.getCurrentZoom();\n assignAttributes(rubberBox, {\n x: startX * currentZoom,\n y: startY * currentZoom,\n width: 0,\n height: 0,\n display: 'inline'\n }, 100);\n }\n },\n mouseMove (mouseX, mouseY) {\n const currentZoom = editorContext_.getCurrentZoom();\n hasMoved = true;\n const drawnPath = editorContext_.getDrawnPath();\n if (editorContext_.getCurrentMode() === 'path') {\n if (!drawnPath) { return; }\n const seglist = drawnPath.pathSegList;\n const index = seglist.numberOfItems - 1;\n\n if (newPoint) {\n // First point\n // if (!index) { return; }\n\n // Set control points\n const pointGrip1 = addCtrlGrip('1c1');\n const pointGrip2 = addCtrlGrip('0c2');\n\n // dragging pointGrip1\n pointGrip1.setAttribute('cx', mouseX);\n pointGrip1.setAttribute('cy', mouseY);\n pointGrip1.setAttribute('display', 'inline');\n\n const ptX = newPoint[0];\n const ptY = newPoint[1];\n\n // set curve\n // const seg = seglist.getItem(index);\n const curX = mouseX / currentZoom;\n const curY = mouseY / currentZoom;\n const altX = (ptX + (ptX - curX));\n const altY = (ptY + (ptY - curY));\n\n pointGrip2.setAttribute('cx', altX * currentZoom);\n pointGrip2.setAttribute('cy', altY * currentZoom);\n pointGrip2.setAttribute('display', 'inline');\n\n const ctrlLine = getCtrlLine(1);\n assignAttributes(ctrlLine, {\n x1: mouseX,\n y1: mouseY,\n x2: altX * currentZoom,\n y2: altY * currentZoom,\n display: 'inline'\n });\n\n if (index === 0) {\n firstCtrl = [mouseX, mouseY];\n } else {\n const last = seglist.getItem(index - 1);\n let lastX = last.x;\n let lastY = last.y;\n\n if (last.pathSegType === 6) {\n lastX += (lastX - last.x2);\n lastY += (lastY - last.y2);\n } else if (firstCtrl) {\n lastX = firstCtrl[0] / currentZoom;\n lastY = firstCtrl[1] / currentZoom;\n }\n replacePathSeg(6, index, [ptX, ptY, lastX, lastY, altX, altY], drawnPath);\n }\n } else {\n const stretchy = getElem('path_stretch_line');\n if (stretchy) {\n const prev = seglist.getItem(index);\n if (prev.pathSegType === 6) {\n const prevX = prev.x + (prev.x - prev.x2);\n const prevY = prev.y + (prev.y - prev.y2);\n replacePathSeg(6, 1, [mouseX, mouseY, prevX * currentZoom, prevY * currentZoom, mouseX, mouseY], stretchy);\n } else if (firstCtrl) {\n replacePathSeg(6, 1, [mouseX, mouseY, firstCtrl[0], firstCtrl[1], mouseX, mouseY], stretchy);\n } else {\n replacePathSeg(4, 1, [mouseX, mouseY], stretchy);\n }\n }\n }\n return;\n }\n // if we are dragging a point, let's move it\n if (path.dragging) {\n const pt = getPointFromGrip({\n x: path.dragging[0],\n y: path.dragging[1]\n }, path);\n const mpt = getPointFromGrip({\n x: mouseX,\n y: mouseY\n }, path);\n const diffX = mpt.x - pt.x;\n const diffY = mpt.y - pt.y;\n path.dragging = [mouseX, mouseY];\n\n if (path.dragctrl) {\n path.moveCtrl(diffX, diffY);\n } else {\n path.movePts(diffX, diffY);\n }\n } else {\n path.selected_pts = [];\n path.eachSeg(function (i) {\n const seg = this;\n if (!seg.next && !seg.prev) { return; }\n\n // const {item} = seg;\n const rubberBox = editorContext_.getRubberBox();\n const rbb = rubberBox.getBBox();\n\n const pt = getGripPt(seg);\n const ptBb = {\n x: pt.x,\n y: pt.y,\n width: 0,\n height: 0\n };\n\n const sel = rectsIntersect(rbb, ptBb);\n\n this.select(sel);\n // Note that addPtsToSelection is not being run\n if (sel) { path.selected_pts.push(seg.index); }\n });\n }\n },\n mouseUp (evt, element, mouseX, mouseY) {\n const drawnPath = editorContext_.getDrawnPath();\n // Create mode\n if (editorContext_.getCurrentMode() === 'path') {\n newPoint = null;\n if (!drawnPath) {\n element = getElem(editorContext_.getId());\n editorContext_.setStarted(false);\n firstCtrl = null;\n }\n\n return {\n keep: true,\n element\n };\n }\n\n // Edit mode\n const rubberBox = editorContext_.getRubberBox();\n if (path.dragging) {\n const lastPt = path.cur_pt;\n\n path.dragging = false;\n path.dragctrl = false;\n path.update();\n\n if (hasMoved) {\n path.endChanges('Move path point(s)');\n }\n\n if (!evt.shiftKey && !hasMoved) {\n path.selectPt(lastPt);\n }\n } else if (rubberBox && rubberBox.getAttribute('display') !== 'none') {\n // Done with multi-node-select\n rubberBox.setAttribute('display', 'none');\n\n if (rubberBox.getAttribute('width') <= 2 && rubberBox.getAttribute('height') <= 2) {\n pathActions.toSelectMode(evt.target);\n }\n\n // else, move back to select mode\n } else {\n pathActions.toSelectMode(evt.target);\n }\n hasMoved = false;\n },\n toEditMode (element) {\n path = getPath_(element);\n editorContext_.setCurrentMode('pathedit');\n editorContext_.clearSelection();\n path.show(true).update();\n path.oldbbox = utilsGetBBox(path.elem);\n subpath = false;\n },\n toSelectMode (elem) {\n const selPath = (elem === path.elem);\n editorContext_.setCurrentMode('select');\n path.show(false);\n currentPath = false;\n editorContext_.clearSelection();\n\n if (path.matrix) {\n // Rotated, so may need to re-calculate the center\n recalcRotatedPath();\n }\n\n if (selPath) {\n editorContext_.call('selected', [elem]);\n editorContext_.addToSelection([elem], true);\n }\n },\n addSubPath (on) {\n if (on) {\n // Internally we go into \"path\" mode, but in the UI it will\n // still appear as if in \"pathedit\" mode.\n editorContext_.setCurrentMode('path');\n subpath = true;\n } else {\n pathActions.clear(true);\n pathActions.toEditMode(path.elem);\n }\n },\n select (target) {\n if (currentPath === target) {\n pathActions.toEditMode(target);\n editorContext_.setCurrentMode('pathedit');\n // going into pathedit mode\n } else {\n currentPath = target;\n }\n },\n reorient () {\n const elem = editorContext_.getSelectedElements()[0];\n if (!elem) { return; }\n const angle = getRotationAngle(elem);\n if (angle === 0) { return; }\n\n const batchCmd = new BatchCommand('Reorient path');\n const changes = {\n d: elem.getAttribute('d'),\n transform: elem.getAttribute('transform')\n };\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));\n editorContext_.clearSelection();\n this.resetOrientation(elem);\n\n editorContext_.addCommandToHistory(batchCmd);\n\n // Set matrix to null\n getPath_(elem).show(false).matrix = null;\n\n this.clear();\n\n editorContext_.addToSelection([elem], true);\n editorContext_.call('changed', editorContext_.getSelectedElements());\n },\n\n clear (remove) {\n const drawnPath = editorContext_.getDrawnPath();\n currentPath = null;\n if (drawnPath) {\n const elem = getElem(editorContext_.getId());\n $(getElem('path_stretch_line')).remove();\n $(elem).remove();\n $(getElem('pathpointgrip_container')).find('*').attr('display', 'none');\n firstCtrl = null;\n editorContext_.setDrawnPath(null);\n editorContext_.setStarted(false);\n } else if (editorContext_.getCurrentMode() === 'pathedit') {\n this.toSelectMode();\n }\n if (path) { path.init().show(false); }\n },\n resetOrientation (pth) {\n if (pth == null || pth.nodeName !== 'path') { return false; }\n const tlist = getTransformList(pth);\n const m = transformListToTransform(tlist).matrix;\n tlist.clear();\n pth.removeAttribute('transform');\n const segList = pth.pathSegList;\n\n // Opera/win/non-EN throws an error here.\n // TODO: Find out why!\n // Presumed fixed in Opera 10.5, so commented out for now\n\n // try {\n const len = segList.numberOfItems;\n // } catch(err) {\n // const fixed_d = pathActions.convertPath(pth);\n // pth.setAttribute('d', fixed_d);\n // segList = pth.pathSegList;\n // const len = segList.numberOfItems;\n // }\n // let lastX, lastY;\n for (let i = 0; i < len; ++i) {\n const seg = segList.getItem(i);\n const type = seg.pathSegType;\n if (type === 1) { continue; }\n const pts = [];\n $.each(['', 1, 2], function (j, n) {\n const x = seg['x' + n], y = seg['y' + n];\n if (x !== undefined && y !== undefined) {\n const pt = transformPoint(x, y, m);\n pts.splice(pts.length, 0, pt.x, pt.y);\n }\n });\n replacePathSeg(type, i, pts, pth);\n }\n\n reorientGrads(pth, m);\n },\n zoomChange () {\n if (editorContext_.getCurrentMode() === 'pathedit') {\n path.update();\n }\n },\n getNodePoint () {\n const selPt = path.selected_pts.length ? path.selected_pts[0] : 1;\n\n const seg = path.segs[selPt];\n return {\n x: seg.item.x,\n y: seg.item.y,\n type: seg.type\n };\n },\n linkControlPoints (linkPoints) {\n setLinkControlPoints(linkPoints);\n },\n clonePathNode () {\n path.storeD();\n\n const selPts = path.selected_pts;\n // const {segs} = path;\n\n let i = selPts.length;\n const nums = [];\n\n while (i--) {\n const pt = selPts[i];\n path.addSeg(pt);\n\n nums.push(pt + i);\n nums.push(pt + i + 1);\n }\n path.init().addPtsToSelection(nums);\n\n path.endChanges('Clone path node(s)');\n },\n opencloseSubPath () {\n const selPts = path.selected_pts;\n // Only allow one selected node for now\n if (selPts.length !== 1) { return; }\n\n const {elem} = path;\n const list = elem.pathSegList;\n\n // const len = list.numberOfItems;\n\n const index = selPts[0];\n\n let openPt = null;\n let startItem = null;\n\n // Check if subpath is already open\n path.eachSeg(function (i) {\n if (this.type === 2 && i <= index) {\n startItem = this.item;\n }\n if (i <= index) { return true; }\n if (this.type === 2) {\n // Found M first, so open\n openPt = i;\n return false;\n }\n if (this.type === 1) {\n // Found Z first, so closed\n openPt = false;\n return false;\n }\n });\n\n if (openPt == null) {\n // Single path, so close last seg\n openPt = path.segs.length - 1;\n }\n\n if (openPt !== false) {\n // Close this path\n\n // Create a line going to the previous \"M\"\n const newseg = elem.createSVGPathSegLinetoAbs(startItem.x, startItem.y);\n\n const closer = elem.createSVGPathSegClosePath();\n if (openPt === path.segs.length - 1) {\n list.appendItem(newseg);\n list.appendItem(closer);\n } else {\n insertItemBefore(elem, closer, openPt);\n insertItemBefore(elem, newseg, openPt);\n }\n\n path.init().selectPt(openPt + 1);\n return;\n }\n\n // M 1,1 L 2,2 L 3,3 L 1,1 z // open at 2,2\n // M 2,2 L 3,3 L 1,1\n\n // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z\n // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z\n\n const seg = path.segs[index];\n\n if (seg.mate) {\n list.removeItem(index); // Removes last \"L\"\n list.removeItem(index); // Removes the \"Z\"\n path.init().selectPt(index - 1);\n return;\n }\n\n let lastM, zSeg;\n\n // Find this sub-path's closing point and remove\n for (let i = 0; i < list.numberOfItems; i++) {\n const item = list.getItem(i);\n\n if (item.pathSegType === 2) {\n // Find the preceding M\n lastM = i;\n } else if (i === index) {\n // Remove it\n list.removeItem(lastM);\n // index--;\n } else if (item.pathSegType === 1 && index < i) {\n // Remove the closing seg of this subpath\n zSeg = i - 1;\n list.removeItem(i);\n break;\n }\n }\n\n let num = (index - lastM) - 1;\n\n while (num--) {\n insertItemBefore(elem, list.getItem(lastM), zSeg);\n }\n\n const pt = list.getItem(lastM);\n\n // Make this point the new \"M\"\n replacePathSeg(2, lastM, [pt.x, pt.y]);\n\n // i = index; // i is local here, so has no effect; what was the intent for this?\n\n path.init().selectPt(0);\n },\n deletePathNode () {\n if (!pathActions.canDeleteNodes) { return; }\n path.storeD();\n\n const selPts = path.selected_pts;\n\n let i = selPts.length;\n while (i--) {\n const pt = selPts[i];\n path.deleteSeg(pt);\n }\n\n // Cleanup\n const cleanup = function () {\n const segList = path.elem.pathSegList;\n let len = segList.numberOfItems;\n\n const remItems = function (pos, count) {\n while (count--) {\n segList.removeItem(pos);\n }\n };\n\n if (len <= 1) { return true; }\n\n while (len--) {\n const item = segList.getItem(len);\n if (item.pathSegType === 1) {\n const prev = segList.getItem(len - 1);\n const nprev = segList.getItem(len - 2);\n if (prev.pathSegType === 2) {\n remItems(len - 1, 2);\n cleanup();\n break;\n } else if (nprev.pathSegType === 2) {\n remItems(len - 2, 3);\n cleanup();\n break;\n }\n } else if (item.pathSegType === 2) {\n if (len > 0) {\n const prevType = segList.getItem(len - 1).pathSegType;\n // Path has M M\n if (prevType === 2) {\n remItems(len - 1, 1);\n cleanup();\n break;\n // Entire path ends with Z M\n } else if (prevType === 1 && segList.numberOfItems - 1 === len) {\n remItems(len, 1);\n cleanup();\n break;\n }\n }\n }\n }\n return false;\n };\n\n cleanup();\n\n // Completely delete a path with 1 or 0 segments\n if (path.elem.pathSegList.numberOfItems <= 1) {\n pathActions.toSelectMode(path.elem);\n editorContext_.canvas.deleteSelectedElements();\n return;\n }\n\n path.init();\n path.clearSelection();\n\n // TODO: Find right way to select point now\n // path.selectPt(selPt);\n if (window.opera) { // Opera repaints incorrectly\n const cp = $(path.elem);\n cp.attr('d', cp.attr('d'));\n }\n path.endChanges('Delete path node(s)');\n },\n smoothPolylineIntoPath,\n setSegType (v) {\n path.setSegType(v);\n },\n moveNode (attr, newValue) {\n const selPts = path.selected_pts;\n if (!selPts.length) { return; }\n\n path.storeD();\n\n // Get first selected point\n const seg = path.segs[selPts[0]];\n const diff = {x: 0, y: 0};\n diff[attr] = newValue - seg.item[attr];\n\n seg.move(diff.x, diff.y);\n path.endChanges('Move path point');\n },\n fixEnd (elem) {\n // Adds an extra segment if the last seg before a Z doesn't end\n // at its M point\n // M0,0 L0,100 L100,100 z\n const segList = elem.pathSegList;\n const len = segList.numberOfItems;\n let lastM;\n for (let i = 0; i < len; ++i) {\n const item = segList.getItem(i);\n if (item.pathSegType === 2) {\n lastM = item;\n }\n\n if (item.pathSegType === 1) {\n const prev = segList.getItem(i - 1);\n if (prev.x !== lastM.x || prev.y !== lastM.y) {\n // Add an L segment here\n const newseg = elem.createSVGPathSegLinetoAbs(lastM.x, lastM.y);\n insertItemBefore(elem, newseg, i);\n // Can this be done better?\n pathActions.fixEnd(elem);\n break;\n }\n }\n }\n if (isWebkit()) { editorContext_.resetD(elem); }\n },\n // Convert a path to one with only absolute or relative values\n convertPath\n };\n})();\n// end pathActions\n","/* globals jQuery, ActiveXObject */\n/**\n * Package: svgedit.utilities\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Alexis Deveria\n * Copyright(c) 2010 Jeff Schiller\n */\n\nimport './pathseg.js';\nimport RGBColor from './canvg/rgbcolor.js';\nimport jqPluginSVG from './jquery-svg.js'; // Needed for SVG attribute setting and array form with `attr`\nimport {importScript, importModule} from './external/dynamic-import-polyfill/importModule.js';\nimport {NS} from './svgedit.js';\nimport {getTransformList} from './svgtransformlist.js';\nimport {setUnitAttr, getTypeMap} from './units.js';\nimport {convertPath} from './path.js';\nimport {\n hasMatrixTransform, transformListToTransform, transformBox\n} from './math.js';\nimport {\n isWebkit, supportsHVLineContainerBBox, supportsPathBBox, supportsXpath,\n supportsSelectors\n} from './browser.js';\n\n// Constants\nconst $ = jqPluginSVG(jQuery);\n\n// String used to encode base64.\nconst KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';\n\n// Much faster than running getBBox() every time\nconst visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';\nconst visElemsArr = visElems.split(',');\n// const hidElems = 'clipPath,defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath';\n\nlet editorContext_ = null;\nlet domdoc_ = null;\nlet domcontainer_ = null;\nlet svgroot_ = null;\n\nexport const init = function (editorContext) {\n editorContext_ = editorContext;\n domdoc_ = editorContext.getDOMDocument();\n domcontainer_ = editorContext.getDOMContainer();\n svgroot_ = editorContext.getSVGRoot();\n};\n\n/**\n* Converts characters in a string to XML-friendly entities.\n* @example: '&' becomes '&'\n* @param str - The string to be converted\n* @returns {String} The converted string\n*/\nexport const toXml = function (str) {\n // ' is ok in XML, but not HTML\n // > does not normally need escaping, though it can if within a CDATA expression (and preceded by \"]]\")\n return str.replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/'/, ''');\n};\n\n/**\n* Converts XML entities in a string to single characters.\n* @example '&amp;' becomes '&'\n* @param str - The string to be converted\n* @returns The converted string\n*/\nexport const fromXml = function (str) {\n return $('

').html(str).text();\n};\n\n// This code was written by Tyler Akins and has been placed in the\n// public domain. It would be nice if you left this header intact.\n// Base64 code from Tyler Akins -- http://rumkin.com\n\n// schiller: Removed string concatenation in favour of Array.join() optimization,\n// also precalculate the size of the array needed.\n\n// Converts a string to base64\nexport const encode64 = function (input) {\n // base64 strings are 4/3 larger than the original string\n input = encodeUTF8(input); // convert non-ASCII characters\n // input = convertToXMLReferences(input);\n if (window.btoa) {\n return window.btoa(input); // Use native if available\n }\n const output = [];\n output.length = Math.floor((input.length + 2) / 3) * 4;\n\n let i = 0, p = 0;\n do {\n const chr1 = input.charCodeAt(i++);\n const chr2 = input.charCodeAt(i++);\n const chr3 = input.charCodeAt(i++);\n\n const enc1 = chr1 >> 2;\n const enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);\n\n let enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);\n let enc4 = chr3 & 63;\n\n if (isNaN(chr2)) {\n enc3 = enc4 = 64;\n } else if (isNaN(chr3)) {\n enc4 = 64;\n }\n\n output[p++] = KEYSTR.charAt(enc1);\n output[p++] = KEYSTR.charAt(enc2);\n output[p++] = KEYSTR.charAt(enc3);\n output[p++] = KEYSTR.charAt(enc4);\n } while (i < input.length);\n\n return output.join('');\n};\n\n// Converts a string from base64\nexport const decode64 = function (input) {\n if (window.atob) {\n return decodeUTF8(window.atob(input));\n }\n\n // remove all characters that are not A-Z, a-z, 0-9, +, /, or =\n input = input.replace(/[^A-Za-z0-9+/=]/g, '');\n\n let output = '';\n let i = 0;\n\n do {\n const enc1 = KEYSTR.indexOf(input.charAt(i++));\n const enc2 = KEYSTR.indexOf(input.charAt(i++));\n const enc3 = KEYSTR.indexOf(input.charAt(i++));\n const enc4 = KEYSTR.indexOf(input.charAt(i++));\n\n const chr1 = (enc1 << 2) | (enc2 >> 4);\n const chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);\n const chr3 = ((enc3 & 3) << 6) | enc4;\n\n output += String.fromCharCode(chr1);\n\n if (enc3 !== 64) {\n output = output + String.fromCharCode(chr2);\n }\n if (enc4 !== 64) {\n output = output + String.fromCharCode(chr3);\n }\n } while (i < input.length);\n return decodeUTF8(output);\n};\n\nexport const decodeUTF8 = function (argString) {\n return decodeURIComponent(escape(argString));\n};\n\n// codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded\nexport const encodeUTF8 = function (argString) {\n return unescape(encodeURIComponent(argString));\n};\n\n/**\n * convert dataURL to object URL\n * @param {string} dataurl\n * @return {string} object URL or empty string\n */\nexport const dataURLToObjectURL = function (dataurl) {\n if (typeof Uint8Array === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || !URL.createObjectURL) {\n return '';\n }\n const arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],\n bstr = atob(arr[1]);\n let n = bstr.length;\n const u8arr = new Uint8Array(n);\n while (n--) {\n u8arr[n] = bstr.charCodeAt(n);\n }\n const blob = new Blob([u8arr], {type: mime});\n return URL.createObjectURL(blob);\n};\n\n/**\n * get object URL for a blob object\n * @param {Blob} blob A Blob object or File object\n * @return {string} object URL or empty string\n */\nexport const createObjectURL = function (blob) {\n if (!blob || typeof URL === 'undefined' || !URL.createObjectURL) {\n return '';\n }\n return URL.createObjectURL(blob);\n};\n\n/**\n * @property {string} blankPageObjectURL\n */\nexport const blankPageObjectURL = (function () {\n if (typeof Blob === 'undefined') {\n return '';\n }\n const blob = new Blob(['SVG-edit '], {type: 'text/html'});\n return createObjectURL(blob);\n})();\n\n// Converts a string to use XML references\nexport const convertToXMLReferences = function (input) {\n let n,\n output = '';\n for (n = 0; n < input.length; n++) {\n const c = input.charCodeAt(n);\n if (c < 128) {\n output += input[n];\n } else if (c > 127) {\n output += ('&#' + c + ';');\n }\n }\n return output;\n};\n\n// Cross-browser compatible method of converting a string to an XML tree\n// found this function here: http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f\nexport const text2xml = function (sXML) {\n if (sXML.includes('\n* - \n* - \n* @param attrVal - The attribute value as a string\n* @returns {String} String with just the URL, like \"someFile.svg#foo\"\n*/\nexport const getUrlFromAttr = function (attrVal) {\n if (attrVal) {\n // url(\"#somegrad\")\n if (attrVal.startsWith('url(\"')) {\n return attrVal.substring(5, attrVal.indexOf('\"', 6));\n }\n // url('#somegrad')\n if (attrVal.startsWith(\"url('\")) {\n return attrVal.substring(5, attrVal.indexOf(\"'\", 6));\n }\n if (attrVal.startsWith('url(')) {\n return attrVal.substring(4, attrVal.indexOf(')'));\n }\n }\n return null;\n};\n\n/**\n* @returns The given element's xlink:href value\n*/\nexport let getHref = function (elem) {\n return elem.getAttributeNS(NS.XLINK, 'href');\n};\n\n/**\n* Sets the given element's xlink:href value\n* @param elem\n* @param {String} val\n*/\nexport let setHref = function (elem, val) {\n elem.setAttributeNS(NS.XLINK, 'xlink:href', val);\n};\n\n/**\n* @returns The document's <defs> element, create it first if necessary\n*/\nexport const findDefs = function () {\n const svgElement = editorContext_.getSVGContent();\n let defs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs');\n if (defs.length > 0) {\n defs = defs[0];\n } else {\n defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs');\n if (svgElement.firstChild) {\n // first child is a comment, so call nextSibling\n svgElement.insertBefore(defs, svgElement.firstChild.nextSibling);\n } else {\n svgElement.appendChild(defs);\n }\n }\n return defs;\n};\n\n// TODO(codedread): Consider moving the next to functions to bbox.js\n\n/**\n* Get correct BBox for a path in Webkit\n* Converted from code found here:\n* http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html\n* @param path - The path DOM element to get the BBox for\n* @returns A BBox-like object\n*/\nexport const getPathBBox = function (path) {\n const seglist = path.pathSegList;\n const tot = seglist.numberOfItems;\n\n const bounds = [[], []];\n const start = seglist.getItem(0);\n let P0 = [start.x, start.y];\n\n for (let i = 0; i < tot; i++) {\n const seg = seglist.getItem(i);\n\n if (seg.x === undefined) { continue; }\n\n // Add actual points to limits\n bounds[0].push(P0[0]);\n bounds[1].push(P0[1]);\n\n if (seg.x1) {\n const P1 = [seg.x1, seg.y1],\n P2 = [seg.x2, seg.y2],\n P3 = [seg.x, seg.y];\n\n for (let j = 0; j < 2; j++) {\n const calc = function (t) {\n return Math.pow(1 - t, 3) * P0[j] +\n 3 * Math.pow(1 - t, 2) * t * P1[j] +\n 3 * (1 - t) * Math.pow(t, 2) * P2[j] +\n Math.pow(t, 3) * P3[j];\n };\n\n const b = 6 * P0[j] - 12 * P1[j] + 6 * P2[j];\n const a = -3 * P0[j] + 9 * P1[j] - 9 * P2[j] + 3 * P3[j];\n const c = 3 * P1[j] - 3 * P0[j];\n\n if (a === 0) {\n if (b === 0) {\n continue;\n }\n const t = -c / b;\n if (t > 0 && t < 1) {\n bounds[j].push(calc(t));\n }\n continue;\n }\n const b2ac = Math.pow(b, 2) - 4 * c * a;\n if (b2ac < 0) { continue; }\n const t1 = (-b + Math.sqrt(b2ac)) / (2 * a);\n if (t1 > 0 && t1 < 1) { bounds[j].push(calc(t1)); }\n const t2 = (-b - Math.sqrt(b2ac)) / (2 * a);\n if (t2 > 0 && t2 < 1) { bounds[j].push(calc(t2)); }\n }\n P0 = P3;\n } else {\n bounds[0].push(seg.x);\n bounds[1].push(seg.y);\n }\n }\n\n const x = Math.min.apply(null, bounds[0]);\n const w = Math.max.apply(null, bounds[0]) - x;\n const y = Math.min.apply(null, bounds[1]);\n const h = Math.max.apply(null, bounds[1]) - y;\n return {\n x,\n y,\n width: w,\n height: h\n };\n};\n\n/**\n* Get the given/selected element's bounding box object, checking for\n* horizontal/vertical lines (see issue 717)\n* Note that performance is currently terrible, so some way to improve would\n* be great.\n* @param selected - Container or <use> DOM element\n* @returns Bounding box object\n*/\nfunction groupBBFix (selected) {\n if (supportsHVLineContainerBBox()) {\n try { return selected.getBBox(); } catch (e) {}\n }\n const ref = $.data(selected, 'ref');\n let matched = null;\n let ret, copy;\n\n if (ref) {\n copy = $(ref).children().clone().attr('visibility', 'hidden');\n $(svgroot_).append(copy);\n matched = copy.filter('line, path');\n } else {\n matched = $(selected).find('line, path');\n }\n\n let issue = false;\n if (matched.length) {\n matched.each(function () {\n const bb = this.getBBox();\n if (!bb.width || !bb.height) {\n issue = true;\n }\n });\n if (issue) {\n const elems = ref ? copy : $(selected).children();\n ret = getStrokedBBox(elems); // getStrokedBBox defined in svgcanvas\n } else {\n ret = selected.getBBox();\n }\n } else {\n ret = selected.getBBox();\n }\n if (ref) {\n copy.remove();\n }\n return ret;\n}\n\n/**\n* Get the given/selected element's bounding box object, convert it to be more\n* usable when necessary\n* @param elem - Optional DOM element to get the BBox for\n* @returns Bounding box object\n*/\nexport const getBBox = function (elem) {\n const selected = elem || editorContext_.geSelectedElements()[0];\n if (elem.nodeType !== 1) { return null; }\n const elname = selected.nodeName;\n\n let ret = null;\n switch (elname) {\n case 'text':\n if (selected.textContent === '') {\n selected.textContent = 'a'; // Some character needed for the selector to use.\n ret = selected.getBBox();\n selected.textContent = '';\n } else {\n if (selected.getBBox) { ret = selected.getBBox(); }\n }\n break;\n case 'path':\n if (!supportsPathBBox()) {\n ret = getPathBBox(selected);\n } else {\n if (selected.getBBox) { ret = selected.getBBox(); }\n }\n break;\n case 'g':\n case 'a':\n ret = groupBBFix(selected);\n break;\n default:\n\n if (elname === 'use') {\n ret = groupBBFix(selected, true);\n }\n if (elname === 'use' || (elname === 'foreignObject' && isWebkit())) {\n if (!ret) { ret = selected.getBBox(); }\n // This is resolved in later versions of webkit, perhaps we should\n // have a featured detection for correct 'use' behavior?\n // ——————————\n if (!isWebkit()) {\n const bb = {};\n bb.width = ret.width;\n bb.height = ret.height;\n bb.x = ret.x + parseFloat(selected.getAttribute('x') || 0);\n bb.y = ret.y + parseFloat(selected.getAttribute('y') || 0);\n ret = bb;\n }\n } else if (visElemsArr.includes(elname)) {\n if (selected) {\n try {\n ret = selected.getBBox();\n } catch (err) {\n // tspan (and textPath apparently) have no `getBBox` in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=937268\n // Re: Chrome returning bbox for containing text element, see: https://bugs.chromium.org/p/chromium/issues/detail?id=349835\n const extent = selected.getExtentOfChar(0); // pos+dimensions of the first glyph\n const width = selected.getComputedTextLength(); // width of the tspan\n ret = {\n x: extent.x,\n y: extent.y,\n width,\n height: extent.height\n };\n }\n } else {\n // Check if element is child of a foreignObject\n const fo = $(selected).closest('foreignObject');\n if (fo.length) {\n if (fo[0].getBBox) {\n ret = fo[0].getBBox();\n }\n }\n }\n }\n }\n if (ret) {\n ret = bboxToObj(ret);\n }\n\n // get the bounding box from the DOM (which is in that element's coordinate system)\n return ret;\n};\n\n/**\n* Create a path 'd' attribute from path segments.\n* Each segment is an array of the form: [singleChar, [x,y, x,y, ...]]\n* @param pathSegments - An array of path segments to be converted\n* @returns The converted path d attribute.\n*/\nexport const getPathDFromSegments = function (pathSegments) {\n let d = '';\n\n $.each(pathSegments, function (j, seg) {\n const pts = seg[1];\n d += seg[0];\n for (let i = 0; i < pts.length; i += 2) {\n d += (pts[i] + ',' + pts[i + 1]) + ' ';\n }\n });\n\n return d;\n};\n\n/**\n* Make a path 'd' attribute from a simple SVG element shape.\n* @param elem - The element to be converted\n* @returns The path d attribute or `undefined` if the element type is unknown.\n*/\nexport const getPathDFromElement = function (elem) {\n // Possibly the cubed root of 6, but 1.81 works best\n let num = 1.81;\n let d, a, rx, ry;\n switch (elem.tagName) {\n case 'ellipse':\n case 'circle':\n a = $(elem).attr(['rx', 'ry', 'cx', 'cy']);\n const {cx, cy} = a;\n ({rx, ry} = a);\n if (elem.tagName === 'circle') {\n rx = ry = $(elem).attr('r');\n }\n\n d = getPathDFromSegments([\n ['M', [(cx - rx), (cy)]],\n ['C', [(cx - rx), (cy - ry / num), (cx - rx / num), (cy - ry), (cx), (cy - ry)]],\n ['C', [(cx + rx / num), (cy - ry), (cx + rx), (cy - ry / num), (cx + rx), (cy)]],\n ['C', [(cx + rx), (cy + ry / num), (cx + rx / num), (cy + ry), (cx), (cy + ry)]],\n ['C', [(cx - rx / num), (cy + ry), (cx - rx), (cy + ry / num), (cx - rx), (cy)]],\n ['Z', []]\n ]);\n break;\n case 'path':\n d = elem.getAttribute('d');\n break;\n case 'line':\n a = $(elem).attr(['x1', 'y1', 'x2', 'y2']);\n d = 'M' + a.x1 + ',' + a.y1 + 'L' + a.x2 + ',' + a.y2;\n break;\n case 'polyline':\n d = 'M' + elem.getAttribute('points');\n break;\n case 'polygon':\n d = 'M' + elem.getAttribute('points') + ' Z';\n break;\n case 'rect':\n const r = $(elem).attr(['rx', 'ry']);\n ({rx, ry} = r);\n const b = elem.getBBox();\n const {x, y} = b, w = b.width, h = b.height;\n num = 4 - num; // Why? Because!\n\n if (!rx && !ry) {\n // Regular rect\n d = getPathDFromSegments([\n ['M', [x, y]],\n ['L', [x + w, y]],\n ['L', [x + w, y + h]],\n ['L', [x, y + h]],\n ['L', [x, y]],\n ['Z', []]\n ]);\n } else {\n d = getPathDFromSegments([\n ['M', [x, y + ry]],\n ['C', [x, y + ry / num, x + rx / num, y, x + rx, y]],\n ['L', [x + w - rx, y]],\n ['C', [x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry]],\n ['L', [x + w, y + h - ry]],\n ['C', [x + w, y + h - ry / num, x + w - rx / num, y + h, x + w - rx, y + h]],\n ['L', [x + rx, y + h]],\n ['C', [x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry]],\n ['L', [x, y + ry]],\n ['Z', []]\n ]);\n }\n break;\n default:\n break;\n }\n\n return d;\n};\n\n/**\n* Get a set of attributes from an element that is useful for convertToPath.\n* @param elem - The element to be probed\n* @returns {Object} An object with attributes.\n*/\nexport const getExtraAttributesForConvertToPath = function (elem) {\n const attrs = {};\n // TODO: make this list global so that we can properly maintain it\n // TODO: what about @transform, @clip-rule, @fill-rule, etc?\n $.each(['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'], function () {\n const a = elem.getAttribute(this);\n if (a) {\n attrs[this] = a;\n }\n });\n return attrs;\n};\n\n/**\n* Get the BBox of an element-as-path\n* @param elem - The DOM element to be probed\n* @param addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson\n* @param pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.\n* @returns The resulting path's bounding box object.\n*/\nexport const getBBoxOfElementAsPath = function (elem, addSvgElementFromJson, pathActions) {\n const path = addSvgElementFromJson({\n element: 'path',\n attr: getExtraAttributesForConvertToPath(elem)\n });\n\n const eltrans = elem.getAttribute('transform');\n if (eltrans) {\n path.setAttribute('transform', eltrans);\n }\n\n const parent = elem.parentNode;\n if (elem.nextSibling) {\n parent.insertBefore(path, elem);\n } else {\n parent.appendChild(path);\n }\n\n const d = getPathDFromElement(elem);\n if (d) path.setAttribute('d', d);\n else path.parentNode.removeChild(path);\n\n // Get the correct BBox of the new path, then discard it\n pathActions.resetOrientation(path);\n let bb = false;\n try {\n bb = path.getBBox();\n } catch (e) {\n // Firefox fails\n }\n path.parentNode.removeChild(path);\n return bb;\n};\n\n/**\n* Convert selected element to a path.\n* @param elem - The DOM element to be converted\n* @param attrs - Apply attributes to new path. see canvas.convertToPath\n* @param addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson\n* @param pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.\n* @param clearSelection - see canvas.clearSelection\n* @param addToSelection - see canvas.addToSelection\n* @param history - see svgedit.history\n* @param addCommandToHistory - see canvas.addCommandToHistory\n* @returns The converted path element or null if the DOM element was not recognized.\n*/\nexport const convertToPath = function (elem, attrs, addSvgElementFromJson, pathActions, clearSelection, addToSelection, history, addCommandToHistory) {\n const batchCmd = new history.BatchCommand('Convert element to Path');\n\n // Any attribute on the element not covered by the passed-in attributes\n attrs = $.extend({}, attrs, getExtraAttributesForConvertToPath(elem));\n\n const path = addSvgElementFromJson({\n element: 'path',\n attr: attrs\n });\n\n const eltrans = elem.getAttribute('transform');\n if (eltrans) {\n path.setAttribute('transform', eltrans);\n }\n\n const {id} = elem;\n const parent = elem.parentNode;\n if (elem.nextSibling) {\n parent.insertBefore(path, elem);\n } else {\n parent.appendChild(path);\n }\n\n const d = getPathDFromElement(elem);\n if (d) {\n path.setAttribute('d', d);\n\n // Replace the current element with the converted one\n\n // Reorient if it has a matrix\n if (eltrans) {\n const tlist = getTransformList(path);\n if (hasMatrixTransform(tlist)) {\n pathActions.resetOrientation(path);\n }\n }\n\n const {nextSibling} = elem;\n batchCmd.addSubCommand(new history.RemoveElementCommand(elem, nextSibling, parent));\n batchCmd.addSubCommand(new history.InsertElementCommand(path));\n\n clearSelection();\n elem.parentNode.removeChild(elem);\n path.setAttribute('id', id);\n path.removeAttribute('visibility');\n addToSelection([path], true);\n\n addCommandToHistory(batchCmd);\n\n return path;\n } else {\n // the elem.tagName was not recognized, so no \"d\" attribute. Remove it, so we've haven't changed anything.\n path.parentNode.removeChild(path);\n return null;\n }\n};\n\n/**\n* Can the bbox be optimized over the native getBBox? The optimized bbox is the same as the native getBBox when\n* the rotation angle is a multiple of 90 degrees and there are no complex transforms.\n* Getting an optimized bbox can be dramatically slower, so we want to make sure it's worth it.\n*\n* The best example for this is a circle rotate 45 degrees. The circle doesn't get wider or taller when rotated\n* about it's center.\n*\n* The standard, unoptimized technique gets the native bbox of the circle, rotates the box 45 degrees, uses\n* that width and height, and applies any transforms to get the final bbox. This means the calculated bbox\n* is much wider than the original circle. If the angle had been 0, 90, 180, etc. both techniques render the\n* same bbox.\n*\n* The optimization is not needed if the rotation is a multiple 90 degrees. The default technique is to call\n* getBBox then apply the angle and any transforms.\n*\n* @param angle - The rotation angle in degrees\n* @param {Boolean} hasMatrixTransform - True if there is a matrix transform\n* @returns {Boolean} True if the bbox can be optimized.\n*/\nfunction bBoxCanBeOptimizedOverNativeGetBBox (angle, hasMatrixTransform) {\n const angleModulo90 = angle % 90;\n const closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99;\n const closeTo0 = angleModulo90 > -0.001 && angleModulo90 < 0.001;\n return hasMatrixTransform || !(closeTo0 || closeTo90);\n}\n\n/**\n* Get bounding box that includes any transforms.\n* @param elem - The DOM element to be converted\n* @param addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson\n* @param pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.\n* @returns A single bounding box object\n*/\nexport const getBBoxWithTransform = function (elem, addSvgElementFromJson, pathActions) {\n // TODO: Fix issue with rotated groups. Currently they work\n // fine in FF, but not in other browsers (same problem mentioned\n // in Issue 339 comment #2).\n\n let bb = getBBox(elem);\n\n if (!bb) {\n return null;\n }\n\n const tlist = getTransformList(elem);\n const angle = getRotationAngleFromTransformList(tlist);\n const hasMatrixXForm = hasMatrixTransform(tlist);\n\n if (angle || hasMatrixXForm) {\n let goodBb = false;\n if (bBoxCanBeOptimizedOverNativeGetBBox(angle, hasMatrixXForm)) {\n // Get the BBox from the raw path for these elements\n // TODO: why ellipse and not circle\n const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon'];\n if (elemNames.includes(elem.tagName)) {\n bb = goodBb = getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions);\n } else if (elem.tagName === 'rect') {\n // Look for radius\n const rx = elem.getAttribute('rx');\n const ry = elem.getAttribute('ry');\n if (rx || ry) {\n bb = goodBb = getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions);\n }\n }\n }\n\n if (!goodBb) {\n const {matrix} = transformListToTransform(tlist);\n bb = transformBox(bb.x, bb.y, bb.width, bb.height, matrix).aabox;\n\n // Old technique that was exceedingly slow with large documents.\n //\n // Accurate way to get BBox of rotated element in Firefox:\n // Put element in group and get its BBox\n //\n // Must use clone else FF freaks out\n // const clone = elem.cloneNode(true);\n // const g = document.createElementNS(NS.SVG, 'g');\n // const parent = elem.parentNode;\n // parent.appendChild(g);\n // g.appendChild(clone);\n // const bb2 = bboxToObj(g.getBBox());\n // parent.removeChild(g);\n }\n }\n return bb;\n};\n\n// TODO: This is problematic with large stroke-width and, for example, a single horizontal line. The calculated BBox extends way beyond left and right sides.\nfunction getStrokeOffsetForBBox (elem) {\n const sw = elem.getAttribute('stroke-width');\n return (!isNaN(sw) && elem.getAttribute('stroke') !== 'none') ? sw / 2 : 0;\n}\n\n/**\n* Get the bounding box for one or more stroked and/or transformed elements\n* @param elems - Array with DOM elements to check\n* @param addSvgElementFromJson - Function to add the path element to the current layer. See canvas.addSvgElementFromJson\n* @param pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.\n* @returns A single bounding box object\n*/\nexport const getStrokedBBox = function (elems, addSvgElementFromJson, pathActions) {\n if (!elems || !elems.length) { return false; }\n\n let fullBb;\n $.each(elems, function () {\n if (fullBb) { return; }\n if (!this.parentNode) { return; }\n fullBb = getBBoxWithTransform(this, addSvgElementFromJson, pathActions);\n });\n\n // This shouldn't ever happen...\n if (fullBb === undefined) { return null; }\n\n // fullBb doesn't include the stoke, so this does no good!\n // if (elems.length == 1) return fullBb;\n\n let maxX = fullBb.x + fullBb.width;\n let maxY = fullBb.y + fullBb.height;\n let minX = fullBb.x;\n let minY = fullBb.y;\n\n // If only one elem, don't call the potentially slow getBBoxWithTransform method again.\n if (elems.length === 1) {\n const offset = getStrokeOffsetForBBox(elems[0]);\n minX -= offset;\n minY -= offset;\n maxX += offset;\n maxY += offset;\n } else {\n $.each(elems, function (i, elem) {\n const curBb = getBBoxWithTransform(elem, addSvgElementFromJson, pathActions);\n if (curBb) {\n const offset = getStrokeOffsetForBBox(elem);\n minX = Math.min(minX, curBb.x - offset);\n minY = Math.min(minY, curBb.y - offset);\n // TODO: The old code had this test for max, but not min. I suspect this test should be for both min and max\n if (elem.nodeType === 1) {\n maxX = Math.max(maxX, curBb.x + curBb.width + offset);\n maxY = Math.max(maxY, curBb.y + curBb.height + offset);\n }\n }\n });\n }\n\n fullBb.x = minX;\n fullBb.y = minY;\n fullBb.width = maxX - minX;\n fullBb.height = maxY - minY;\n return fullBb;\n};\n\n/**\n* Get all elements that have a BBox (excludes `<defs>`, `<title>`, etc).\n* Note that 0-opacity, off-screen etc elements are still considered \"visible\"\n* for this function\n* @param parent - The parent DOM element to search within\n* @returns {Array} All \"visible\" elements.\n*/\nexport const getVisibleElements = function (parent) {\n if (!parent) {\n parent = $(editorContext_.getSVGContent()).children(); // Prevent layers from being included\n }\n\n const contentElems = [];\n $(parent).children().each(function (i, elem) {\n if (elem.getBBox) {\n contentElems.push(elem);\n }\n });\n return contentElems.reverse();\n};\n\n/**\n* Get the bounding box for one or more stroked and/or transformed elements\n* @param elems - Array with DOM elements to check\n* @returns A single bounding box object\n*/\nexport const getStrokedBBoxDefaultVisible = function (elems) {\n if (!elems) { elems = getVisibleElements(); }\n return getStrokedBBox(\n elems,\n editorContext_.addSvgElementFromJson,\n editorContext_.pathActions\n );\n};\n\n/**\n* Get the rotation angle of the given transform list.\n* @param tlist - List of transforms\n* @param {Boolean} toRad - When true returns the value in radians rather than degrees\n* @returns {Number} Float with the angle in degrees or radians\n*/\nexport const getRotationAngleFromTransformList = function (tlist, toRad) {\n if (!tlist) { return 0; } // elements have no tlist\n const N = tlist.numberOfItems;\n for (let i = 0; i < N; ++i) {\n const xform = tlist.getItem(i);\n if (xform.type === 4) {\n return toRad ? xform.angle * Math.PI / 180.0 : xform.angle;\n }\n }\n return 0.0;\n};\n\n/**\n* Get the rotation angle of the given/selected DOM element\n* @param elem - Optional DOM element to get the angle for\n* @param {Boolean} toRad - When true returns the value in radians rather than degrees\n* @returns {Number} Float with the angle in degrees or radians\n*/\nexport let getRotationAngle = function (elem, toRad) {\n const selected = elem || editorContext_.getSelectedElements()[0];\n // find the rotation transform (if any) and set it\n const tlist = getTransformList(selected);\n return getRotationAngleFromTransformList(tlist, toRad);\n};\n\n/**\n* Get the reference element associated with the given attribute value\n* @param {String} attrVal - The attribute value as a string\n* @returns Reference element\n*/\nexport const getRefElem = function (attrVal) {\n return getElem(getUrlFromAttr(attrVal).substr(1));\n};\n\n/**\n* Get a DOM element by ID within the SVG root element.\n* @param {String} id - String with the element's new ID\n*/\nexport const getElem = (supportsSelectors())\n ? function (id) {\n // querySelector lookup\n return svgroot_.querySelector('#' + id);\n } : supportsXpath()\n ? function (id) {\n // xpath lookup\n return domdoc_.evaluate(\n 'svg:svg[@id=\"svgroot\"]//svg:*[@id=\"' + id + '\"]',\n domcontainer_,\n function () { return NS.SVG; },\n 9,\n null).singleNodeValue;\n }\n : function (id) {\n // jQuery lookup: twice as slow as xpath in FF\n return $(svgroot_).find('[id=' + id + ']')[0];\n };\n\n/**\n* Assigns multiple attributes to an element.\n* @param node - DOM element to apply new attribute values to\n* @param {Object} attrs - Object with attribute keys/values\n* @param {Number} suspendLength - Optional integer of milliseconds to suspend redraw\n* @param {Boolean} unitCheck - Boolean to indicate the need to use svgedit.units.setUnitAttr\n*/\nexport const assignAttributes = function (node, attrs, suspendLength, unitCheck) {\n for (const i in attrs) {\n const ns = (i.substr(0, 4) === 'xml:'\n ? NS.XML\n : i.substr(0, 6) === 'xlink:' ? NS.XLINK : null);\n\n if (ns) {\n node.setAttributeNS(ns, i, attrs[i]);\n } else if (!unitCheck) {\n node.setAttribute(i, attrs[i]);\n } else {\n setUnitAttr(node, i, attrs[i]);\n }\n }\n};\n\n/**\n* Remove unneeded (default) attributes, makes resulting SVG smaller\n* @param element - DOM element to clean up\n*/\nexport const cleanupElement = function (element) {\n const defaults = {\n 'fill-opacity': 1,\n 'stop-opacity': 1,\n opacity: 1,\n stroke: 'none',\n 'stroke-dasharray': 'none',\n 'stroke-linejoin': 'miter',\n 'stroke-linecap': 'butt',\n 'stroke-opacity': 1,\n 'stroke-width': 1,\n rx: 0,\n ry: 0\n };\n\n if (element.nodeName === 'ellipse') {\n // Ellipse elements requires rx and ry attributes\n delete defaults.rx;\n delete defaults.ry;\n }\n\n for (const attr in defaults) {\n const val = defaults[attr];\n if (element.getAttribute(attr) === String(val)) {\n element.removeAttribute(attr);\n }\n }\n};\n\n// round value to for snapping\nexport const snapToGrid = function (value) {\n const unit = editorContext_.getBaseUnit();\n let stepSize = editorContext_.getSnappingStep();\n if (unit !== 'px') {\n stepSize *= getTypeMap()[unit];\n }\n value = Math.round(value / stepSize) * stepSize;\n return value;\n};\n\nexport const regexEscape = function (str, delimiter) {\n // From: http://phpjs.org/functions\n return String(str).replace(new RegExp('[.\\\\\\\\+*?\\\\[\\\\^\\\\]$(){}=!<>|:\\\\' + (delimiter || '') + '-]', 'g'), '\\\\$&');\n};\n\nconst loadedScripts = {};\n/**\n* @param {string} name A global which can be used to determine if the script is already loaded\n* @param {array} scripts An array of scripts to preload (in order)\n* @param {function} cb The callback to execute upon load.\n* @param {object} options Object with `globals` boolean property (if it is not a module)\n*/\nexport const executeAfterLoads = function (name, scripts, cb, options = {globals: false}) {\n return function () {\n const args = arguments;\n function endCallback () {\n cb.apply(null, args);\n }\n const modularVersion = !('svgEditor' in window) ||\n !window.svgEditor ||\n window.svgEditor.modules !== false;\n if (loadedScripts[name] === true) {\n endCallback();\n } else if (Array.isArray(loadedScripts[name])) { // Still loading\n loadedScripts[name].push(endCallback);\n } else {\n loadedScripts[name] = [];\n const importer = modularVersion && !options.globals\n ? importModule\n : importScript;\n scripts.reduce(function (oldProm, script) {\n // Todo: Once `import()` and modules widely supported, switch to it\n return oldProm.then(() => importer(script));\n }, Promise.resolve()).then(function () {\n endCallback();\n loadedScripts[name].forEach((cb) => {\n cb();\n });\n loadedScripts[name] = true;\n })();\n }\n };\n};\n\nexport const buildCanvgCallback = function (callCanvg) {\n return executeAfterLoads('canvg', ['canvg/rgbcolor.js', 'canvg/canvg.js'], callCanvg);\n};\n\nexport const buildJSPDFCallback = function (callJSPDF) {\n return executeAfterLoads('RGBColor', ['canvg/rgbcolor.js'], () => {\n const arr = [];\n if (!RGBColor || RGBColor.ok === undefined) { // It's not our RGBColor, so we'll need to load it\n arr.push('canvg/rgbcolor.js');\n }\n executeAfterLoads('jsPDF', [\n ...arr,\n 'jspdf/underscore-min.js',\n 'jspdf/jspdf.min.js',\n 'jspdf/jspdf.plugin.svgToPdf.js'\n ], callJSPDF, {globals: true})();\n });\n};\n\n/**\n * Prevents default browser click behaviour on the given element\n * @param img - The DOM element to prevent the click on\n */\nexport const preventClickDefault = function (img) {\n $(img).click(function (e) { e.preventDefault(); });\n};\n\n/**\n * Create a clone of an element, updating its ID and its children's IDs when needed\n * @param {Element} el - DOM element to clone\n * @param {function()} getNextId - function the get the next unique ID.\n * @returns {Element}\n */\nexport const copyElem = function (el, getNextId) {\n // manually create a copy of the element\n const newEl = document.createElementNS(el.namespaceURI, el.nodeName);\n $.each(el.attributes, function (i, attr) {\n if (attr.localName !== '-moz-math-font-style') {\n newEl.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value);\n }\n });\n // set the copied element's new id\n newEl.removeAttribute('id');\n newEl.id = getNextId();\n\n // Opera's \"d\" value needs to be reset for Opera/Win/non-EN\n // Also needed for webkit (else does not keep curved segments on clone)\n if (isWebkit() && el.nodeName === 'path') {\n const fixedD = convertPath(el);\n newEl.setAttribute('d', fixedD);\n }\n\n // now create copies of all children\n $.each(el.childNodes, function (i, child) {\n switch (child.nodeType) {\n case 1: // element node\n newEl.appendChild(copyElem(child, getNextId));\n break;\n case 3: // text node\n newEl.textContent = child.nodeValue;\n break;\n default:\n break;\n }\n });\n\n if ($(el).data('gsvg')) {\n $(newEl).data('gsvg', newEl.firstChild);\n } else if ($(el).data('symbol')) {\n const ref = $(el).data('symbol');\n $(newEl).data('ref', ref).data('symbol', ref);\n } else if (newEl.tagName === 'image') {\n preventClickDefault(newEl);\n }\n\n return newEl;\n};\n\n// Unit testing\nexport const mock = ({\n getHref: getHrefUser, setHref: setHrefUser, getRotationAngle: getRotationAngleUser\n}) => {\n getHref = getHrefUser;\n setHref = setHrefUser;\n getRotationAngle = getRotationAngleUser;\n};\n","/* globals jQuery */\n/**\n * Package: svgedit.contextmenu\n *\n * Licensed under the Apache License, Version 2\n *\n * Author: Adam Bender\n */\n// Dependencies:\n// 1) jQuery (for dom injection of context menus)\n\nconst $ = jQuery;\n\nlet contextMenuExtensions = {};\n\nconst menuItemIsValid = function (menuItem) {\n return menuItem && menuItem.id && menuItem.label && menuItem.action && typeof menuItem.action === 'function';\n};\nexport const add = function (menuItem) {\n // menuItem: {id, label, shortcut, action}\n if (!menuItemIsValid(menuItem)) {\n console.error('Menu items must be defined and have at least properties: id, label, action, where action must be a function');\n return;\n }\n if (menuItem.id in contextMenuExtensions) {\n console.error('Cannot add extension \"' + menuItem.id + '\", an extension by that name already exists\"');\n return;\n }\n // Register menuItem action, see below for deferred menu dom injection\n console.log('Registed contextmenu item: {id:' + menuItem.id + ', label:' + menuItem.label + '}');\n contextMenuExtensions[menuItem.id] = menuItem;\n // TODO: Need to consider how to handle custom enable/disable behavior\n};\nexport const hasCustomHandler = function (handlerKey) {\n return Boolean(contextMenuExtensions[handlerKey]);\n};\nexport const getCustomHandler = function (handlerKey) {\n return contextMenuExtensions[handlerKey].action;\n};\nconst injectExtendedContextMenuItemIntoDom = function (menuItem) {\n if (!Object.keys(contextMenuExtensions).length) {\n // all menuItems appear at the bottom of the menu in their own container.\n // if this is the first extension menu we need to add the separator.\n $('#cmenu_canvas').append(\"

  • \");\n }\n const shortcut = menuItem.shortcut || '';\n $('#cmenu_canvas').append(\"
  • \" +\n menuItem.label + \"\" +\n shortcut + '
  • ');\n};\n\nexport const injectExtendedContextMenuItemsIntoDom = function () {\n for (const menuItem in contextMenuExtensions) {\n injectExtendedContextMenuItemIntoDom(contextMenuExtensions[menuItem]);\n }\n};\nexport const resetCustomMenus = function () { contextMenuExtensions = {}; };\n","/* eslint-disable new-cap */\n/* globals stackBlurCanvasRGBA, ActiveXObject */\n/*\n * canvg.js - Javascript SVG parser and renderer on Canvas\n * MIT Licensed\n * Gabe Lerner (gabelerner@gmail.com)\n * http://code.google.com/p/canvg/\n */\n\nimport RGBColor from './rgbcolor.js';\n\n// canvg(target, s)\n// empty parameters: replace all 'svg' elements on page with 'canvas' elements\n// target: canvas element or the id of a canvas element\n// s: svg string, url to svg file, or xml document\n// opts: optional hash of options\n// ignoreMouse: true => ignore mouse events\n// ignoreAnimation: true => ignore animations\n// ignoreDimensions: true => does not try to resize canvas\n// ignoreClear: true => does not clear canvas\n// offsetX: int => draws at a x offset\n// offsetY: int => draws at a y offset\n// scaleWidth: int => scales horizontally to width\n// scaleHeight: int => scales vertically to height\n// renderCallback: function => will call the function after the first render is completed\n// forceRedraw: function => will call the function on every frame, if it returns true, will redraw\nexport default function canvg (target, s, opts) {\n // no parameters\n if (target == null && s == null && opts == null) {\n const svgTags = document.querySelectorAll('svg');\n for (let i = 0; i < svgTags.length; i++) {\n const svgTag = svgTags[i];\n const c = document.createElement('canvas');\n c.width = svgTag.clientWidth;\n c.height = svgTag.clientHeight;\n svgTag.parentNode.insertBefore(c, svgTag);\n svgTag.parentNode.removeChild(svgTag);\n const div = document.createElement('div');\n div.appendChild(svgTag);\n canvg(c, div.innerHTML);\n }\n return;\n }\n\n if (typeof target === 'string') {\n target = document.getElementById(target);\n }\n\n // store class on canvas\n if (target.svg != null) target.svg.stop();\n const svg = build(opts || {});\n // on i.e. 8 for flash canvas, we can't assign the property so check for it\n if (!(target.childNodes.length === 1 && target.childNodes[0].nodeName === 'OBJECT')) {\n target.svg = svg;\n }\n\n const ctx = target.getContext('2d');\n if (typeof s.documentElement !== 'undefined') {\n // load from xml doc\n svg.loadXmlDoc(ctx, s);\n } else if (s.substr(0, 1) === '<') {\n // load from xml string\n svg.loadXml(ctx, s);\n } else {\n // load from url\n svg.load(ctx, s);\n }\n}\n\nfunction build (opts) {\n const svg = {opts};\n\n svg.FRAMERATE = 30;\n svg.MAX_VIRTUAL_PIXELS = 30000;\n\n svg.log = function (msg) {};\n if (svg.opts.log === true && typeof console !== 'undefined') {\n svg.log = function (msg) { console.log(msg); };\n }\n\n // globals\n svg.init = function (ctx) {\n let uniqueId = 0;\n svg.UniqueId = function () { uniqueId++; return 'canvg' + uniqueId; };\n svg.Definitions = {};\n svg.Styles = {};\n svg.Animations = [];\n svg.Images = [];\n svg.ctx = ctx;\n svg.ViewPort = new function () {\n this.viewPorts = [];\n this.Clear = function () { this.viewPorts = []; };\n this.SetCurrent = function (width, height) { this.viewPorts.push({ width, height }); };\n this.RemoveCurrent = function () { this.viewPorts.pop(); };\n this.Current = function () { return this.viewPorts[this.viewPorts.length - 1]; };\n this.width = function () { return this.Current().width; };\n this.height = function () { return this.Current().height; };\n this.ComputeSize = function (d) {\n if (d != null && typeof d === 'number') return d;\n if (d === 'x') return this.width();\n if (d === 'y') return this.height();\n return Math.sqrt(Math.pow(this.width(), 2) + Math.pow(this.height(), 2)) / Math.sqrt(2);\n };\n }();\n };\n svg.init();\n\n // images loaded\n svg.ImagesLoaded = function () {\n for (let i = 0; i < svg.Images.length; i++) {\n if (!svg.Images[i].loaded) return false;\n }\n return true;\n };\n\n // trim\n svg.trim = function (s) { return s.replace(/^\\s+|\\s+$/g, ''); };\n\n // compress spaces\n svg.compressSpaces = function (s) { return s.replace(/[\\s\\r\\t\\n]+/gm, ' '); };\n\n // ajax\n svg.ajax = function (url) {\n const AJAX = window.XMLHttpRequest\n ? new XMLHttpRequest()\n : new ActiveXObject('Microsoft.XMLHTTP');\n\n if (AJAX) {\n AJAX.open('GET', url, false);\n AJAX.send(null);\n return AJAX.responseText;\n }\n return null;\n };\n\n // parse xml\n svg.parseXml = function (xml) {\n if (window.DOMParser) {\n const parser = new DOMParser();\n return parser.parseFromString(xml, 'text/xml');\n } else {\n xml = xml.replace(/]*>/, '');\n const xmlDoc = new ActiveXObject('Microsoft.XMLDOM');\n xmlDoc.async = 'false';\n xmlDoc.loadXML(xml);\n return xmlDoc;\n }\n };\n\n // text extensions\n // get the text baseline\n const textBaselineMapping = {\n baseline: 'alphabetic',\n 'before-edge': 'top',\n 'text-before-edge': 'top',\n middle: 'middle',\n central: 'middle',\n 'after-edge': 'bottom',\n 'text-after-edge': 'bottom',\n ideographic: 'ideographic',\n alphabetic: 'alphabetic',\n hanging: 'hanging',\n mathematical: 'alphabetic'\n };\n\n svg.Property = class Property {\n constructor (name, value) {\n this.name = name;\n this.value = value;\n }\n\n getValue () {\n return this.value;\n }\n\n hasValue () {\n return (this.value != null && this.value !== '');\n }\n\n // return the numerical value of the property\n numValue () {\n if (!this.hasValue()) return 0;\n\n let n = parseFloat(this.value);\n if ((this.value + '').match(/%$/)) {\n n = n / 100.0;\n }\n return n;\n }\n\n valueOrDefault (def) {\n if (this.hasValue()) return this.value;\n return def;\n }\n\n numValueOrDefault (def) {\n if (this.hasValue()) return this.numValue();\n return def;\n }\n\n // color extensions\n // augment the current color value with the opacity\n addOpacity (opacityProp) {\n let newValue = this.value;\n if (opacityProp.value != null && opacityProp.value !== '' && typeof this.value === 'string') { // can only add opacity to colors, not patterns\n const color = new RGBColor(this.value);\n if (color.ok) {\n newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')';\n }\n }\n return new svg.Property(this.name, newValue);\n }\n\n // definition extensions\n // get the definition from the definitions table\n getDefinition () {\n let name = this.value.match(/#([^)'\"]+)/);\n if (name) { name = name[1]; }\n if (!name) { name = this.value; }\n return svg.Definitions[name];\n }\n\n isUrlDefinition () {\n return this.value.startsWith('url(');\n }\n\n getFillStyleDefinition (e, opacityProp) {\n let def = this.getDefinition();\n\n // gradient\n if (def != null && def.createGradient) {\n return def.createGradient(svg.ctx, e, opacityProp);\n }\n\n // pattern\n if (def != null && def.createPattern) {\n if (def.getHrefAttribute().hasValue()) {\n const pt = def.attribute('patternTransform');\n def = def.getHrefAttribute().getDefinition();\n if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; }\n }\n return def.createPattern(svg.ctx, e);\n }\n\n return null;\n }\n\n // length extensions\n getDPI (viewPort) {\n return 96.0; // TODO: compute?\n }\n\n getEM (viewPort) {\n let em = 12;\n\n const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize);\n if (fontSize.hasValue()) em = fontSize.toPixels(viewPort);\n\n return em;\n }\n\n getUnits () {\n const s = this.value + '';\n return s.replace(/[0-9.-]/g, '');\n }\n\n // get the length as pixels\n toPixels (viewPort, processPercent) {\n if (!this.hasValue()) return 0;\n const s = this.value + '';\n if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort);\n if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0;\n if (s.match(/px$/)) return this.numValue();\n if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0);\n if (s.match(/pc$/)) return this.numValue() * 15;\n if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54;\n if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4;\n if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort);\n if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort);\n const n = this.numValue();\n if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort);\n return n;\n }\n\n // time extensions\n // get the time as milliseconds\n toMilliseconds () {\n if (!this.hasValue()) return 0;\n const s = this.value + '';\n if (s.match(/s$/)) return this.numValue() * 1000;\n if (s.match(/ms$/)) return this.numValue();\n return this.numValue();\n }\n\n // angle extensions\n // get the angle as radians\n toRadians () {\n if (!this.hasValue()) return 0;\n const s = this.value + '';\n if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0);\n if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0);\n if (s.match(/rad$/)) return this.numValue();\n return this.numValue() * (Math.PI / 180.0);\n }\n\n toTextBaseline () {\n if (!this.hasValue()) return null;\n return textBaselineMapping[this.value];\n }\n };\n\n // fonts\n svg.Font = new function () {\n this.Styles = 'normal|italic|oblique|inherit';\n this.Variants = 'normal|small-caps|inherit';\n this.Weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit';\n\n this.CreateFont = function (fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) {\n const f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font);\n return {\n fontFamily: fontFamily || f.fontFamily,\n fontSize: fontSize || f.fontSize,\n fontStyle: fontStyle || f.fontStyle,\n fontWeight: fontWeight || f.fontWeight,\n fontVariant: fontVariant || f.fontVariant,\n toString () {\n return [\n this.fontStyle, this.fontVariant, this.fontWeight,\n this.fontSize, this.fontFamily\n ].join(' ');\n }\n };\n };\n\n const that = this;\n this.Parse = function (s) {\n const f = {};\n const d = svg.trim(svg.compressSpaces(s || '')).split(' ');\n const set = {fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false};\n let ff = '';\n for (let i = 0; i < d.length; i++) {\n if (!set.fontStyle && that.Styles.includes(d[i])) {\n if (d[i] !== 'inherit') f.fontStyle = d[i]; set.fontStyle = true;\n } else if (!set.fontVariant && that.Variants.includes(d[i])) {\n if (d[i] !== 'inherit') f.fontVariant = d[i]; set.fontStyle = set.fontVariant = true;\n } else if (!set.fontWeight && that.Weights.includes(d[i])) {\n if (d[i] !== 'inherit') f.fontWeight = d[i]; set.fontStyle = set.fontVariant = set.fontWeight = true;\n } else if (!set.fontSize) {\n if (d[i] !== 'inherit') f.fontSize = d[i].split('/')[0]; set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true;\n } else {\n if (d[i] !== 'inherit') ff += d[i];\n }\n }\n if (ff !== '') f.fontFamily = ff;\n return f;\n };\n }();\n\n // points and paths\n svg.ToNumberArray = function (s) {\n const a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' ');\n for (let i = 0; i < a.length; i++) {\n a[i] = parseFloat(a[i]);\n }\n return a;\n };\n svg.Point = class {\n constructor (x, y) {\n this.x = x;\n this.y = y;\n }\n\n angleTo (p) {\n return Math.atan2(p.y - this.y, p.x - this.x);\n }\n\n applyTransform (v) {\n const xp = this.x * v[0] + this.y * v[2] + v[4];\n const yp = this.x * v[1] + this.y * v[3] + v[5];\n this.x = xp;\n this.y = yp;\n }\n };\n\n svg.CreatePoint = function (s) {\n const a = svg.ToNumberArray(s);\n return new svg.Point(a[0], a[1]);\n };\n svg.CreatePath = function (s) {\n const a = svg.ToNumberArray(s);\n const path = [];\n for (let i = 0; i < a.length; i += 2) {\n path.push(new svg.Point(a[i], a[i + 1]));\n }\n return path;\n };\n\n // bounding box\n svg.BoundingBox = function (x1, y1, x2, y2) { // pass in initial points if you want\n this.x1 = Number.NaN;\n this.y1 = Number.NaN;\n this.x2 = Number.NaN;\n this.y2 = Number.NaN;\n\n this.x = function () { return this.x1; };\n this.y = function () { return this.y1; };\n this.width = function () { return this.x2 - this.x1; };\n this.height = function () { return this.y2 - this.y1; };\n\n this.addPoint = function (x, y) {\n if (x != null) {\n if (isNaN(this.x1) || isNaN(this.x2)) {\n this.x1 = x;\n this.x2 = x;\n }\n if (x < this.x1) this.x1 = x;\n if (x > this.x2) this.x2 = x;\n }\n\n if (y != null) {\n if (isNaN(this.y1) || isNaN(this.y2)) {\n this.y1 = y;\n this.y2 = y;\n }\n if (y < this.y1) this.y1 = y;\n if (y > this.y2) this.y2 = y;\n }\n };\n this.addX = function (x) { this.addPoint(x, null); };\n this.addY = function (y) { this.addPoint(null, y); };\n\n this.addBoundingBox = function (bb) {\n this.addPoint(bb.x1, bb.y1);\n this.addPoint(bb.x2, bb.y2);\n };\n\n this.addQuadraticCurve = function (p0x, p0y, p1x, p1y, p2x, p2y) {\n const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0)\n const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0)\n const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0)\n const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0)\n this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y);\n };\n\n this.addBezierCurve = function (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {\n // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html\n const p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y];\n this.addPoint(p0[0], p0[1]);\n this.addPoint(p3[0], p3[1]);\n\n for (let i = 0; i <= 1; i++) {\n const f = function (t) {\n return Math.pow(1 - t, 3) * p0[i] +\n 3 * Math.pow(1 - t, 2) * t * p1[i] +\n 3 * (1 - t) * Math.pow(t, 2) * p2[i] +\n Math.pow(t, 3) * p3[i];\n };\n\n const b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i];\n const a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i];\n const c = 3 * p1[i] - 3 * p0[i];\n\n if (a === 0) {\n if (b === 0) continue;\n const t = -c / b;\n if (t > 0 && t < 1) {\n if (i === 0) this.addX(f(t));\n if (i === 1) this.addY(f(t));\n }\n continue;\n }\n\n const b2ac = Math.pow(b, 2) - 4 * c * a;\n if (b2ac < 0) continue;\n const t1 = (-b + Math.sqrt(b2ac)) / (2 * a);\n if (t1 > 0 && t1 < 1) {\n if (i === 0) this.addX(f(t1));\n if (i === 1) this.addY(f(t1));\n }\n const t2 = (-b - Math.sqrt(b2ac)) / (2 * a);\n if (t2 > 0 && t2 < 1) {\n if (i === 0) this.addX(f(t2));\n if (i === 1) this.addY(f(t2));\n }\n }\n };\n\n this.isPointInBox = function (x, y) {\n return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2);\n };\n\n this.addPoint(x1, y1);\n this.addPoint(x2, y2);\n };\n\n // transforms\n svg.Transform = function (v) {\n this.Type = {};\n\n // translate\n this.Type.translate = function (s) {\n this.p = svg.CreatePoint(s);\n this.apply = function (ctx) {\n ctx.translate(this.p.x || 0.0, this.p.y || 0.0);\n };\n this.unapply = function (ctx) {\n ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0);\n };\n this.applyToPoint = function (p) {\n p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]);\n };\n };\n\n // rotate\n this.Type.rotate = function (s) {\n const a = svg.ToNumberArray(s);\n this.angle = new svg.Property('angle', a[0]);\n this.cx = a[1] || 0;\n this.cy = a[2] || 0;\n this.apply = function (ctx) {\n ctx.translate(this.cx, this.cy);\n ctx.rotate(this.angle.toRadians());\n ctx.translate(-this.cx, -this.cy);\n };\n this.unapply = function (ctx) {\n ctx.translate(this.cx, this.cy);\n ctx.rotate(-1.0 * this.angle.toRadians());\n ctx.translate(-this.cx, -this.cy);\n };\n this.applyToPoint = function (p) {\n const a = this.angle.toRadians();\n p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]);\n p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]);\n p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]);\n };\n };\n\n this.Type.scale = function (s) {\n this.p = svg.CreatePoint(s);\n this.apply = function (ctx) {\n ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0);\n };\n this.unapply = function (ctx) {\n ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0);\n };\n this.applyToPoint = function (p) {\n p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]);\n };\n };\n\n this.Type.matrix = function (s) {\n this.m = svg.ToNumberArray(s);\n this.apply = function (ctx) {\n ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]);\n };\n this.applyToPoint = function (p) {\n p.applyTransform(this.m);\n };\n };\n\n this.Type.SkewBase = class extends this.Type.matrix {\n constructor (s) {\n super(s);\n this.angle = new svg.Property('angle', s);\n }\n };\n\n this.Type.skewX = class extends this.Type.SkewBase {\n constructor (s) {\n super(s);\n this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0];\n }\n };\n\n this.Type.skewY = class extends this.Type.SkewBase {\n constructor (s) {\n super(s);\n this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0];\n }\n };\n\n this.transforms = [];\n\n this.apply = function (ctx) {\n for (let i = 0; i < this.transforms.length; i++) {\n this.transforms[i].apply(ctx);\n }\n };\n\n this.unapply = function (ctx) {\n for (let i = this.transforms.length - 1; i >= 0; i--) {\n this.transforms[i].unapply(ctx);\n }\n };\n\n this.applyToPoint = function (p) {\n for (let i = 0; i < this.transforms.length; i++) {\n this.transforms[i].applyToPoint(p);\n }\n };\n\n const data = svg.trim(svg.compressSpaces(v)).replace(/\\)([a-zA-Z])/g, ') $1').replace(/\\)(\\s?,\\s?)/g, ') ').split(/\\s(?=[a-z])/);\n for (let i = 0; i < data.length; i++) {\n const type = svg.trim(data[i].split('(')[0]);\n const s = data[i].split('(')[1].replace(')', '');\n const transform = new this.Type[type](s);\n transform.type = type;\n this.transforms.push(transform);\n }\n };\n\n // aspect ratio\n svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) {\n // aspect ratio - https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute\n aspectRatio = svg.compressSpaces(aspectRatio);\n aspectRatio = aspectRatio.replace(/^defer\\s/, ''); // ignore defer\n const align = aspectRatio.split(' ')[0] || 'xMidYMid';\n const meetOrSlice = aspectRatio.split(' ')[1] || 'meet';\n\n // calculate scale\n const scaleX = width / desiredWidth;\n const scaleY = height / desiredHeight;\n const scaleMin = Math.min(scaleX, scaleY);\n const scaleMax = Math.max(scaleX, scaleY);\n if (meetOrSlice === 'meet') { desiredWidth *= scaleMin; desiredHeight *= scaleMin; }\n if (meetOrSlice === 'slice') { desiredWidth *= scaleMax; desiredHeight *= scaleMax; }\n\n refX = new svg.Property('refX', refX);\n refY = new svg.Property('refY', refY);\n if (refX.hasValue() && refY.hasValue()) {\n ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y'));\n } else {\n // align\n if (align.match(/^xMid/) && ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0);\n if (align.match(/YMid$/) && ((meetOrSlice === 'meet' && scaleMin === scaleX) || (meetOrSlice === 'slice' && scaleMax === scaleX))) ctx.translate(0, height / 2.0 - desiredHeight / 2.0);\n if (align.match(/^xMax/) && ((meetOrSlice === 'meet' && scaleMin === scaleY) || (meetOrSlice === 'slice' && scaleMax === scaleY))) ctx.translate(width - desiredWidth, 0);\n if (align.match(/YMax$/) && ((meetOrSlice === 'meet' && scaleMin === scaleX) || (meetOrSlice === 'slice' && scaleMax === scaleX))) ctx.translate(0, height - desiredHeight);\n }\n\n // scale\n if (align === 'none') ctx.scale(scaleX, scaleY);\n else if (meetOrSlice === 'meet') ctx.scale(scaleMin, scaleMin);\n else if (meetOrSlice === 'slice') ctx.scale(scaleMax, scaleMax);\n\n // translate\n ctx.translate(minX == null ? 0 : -minX, minY == null ? 0 : -minY);\n };\n\n // elements\n svg.Element = {};\n\n svg.EmptyProperty = new svg.Property('EMPTY', '');\n\n svg.Element.ElementBase = function (node) {\n this.attributes = {};\n this.styles = {};\n this.children = [];\n\n // get or create attribute\n this.attribute = function (name, createIfNotExists) {\n let a = this.attributes[name];\n if (a != null) return a;\n\n if (createIfNotExists === true) { a = new svg.Property(name, ''); this.attributes[name] = a; }\n return a || svg.EmptyProperty;\n };\n\n this.getHrefAttribute = function () {\n for (const a in this.attributes) {\n if (a.match(/:href$/)) {\n return this.attributes[a];\n }\n }\n return svg.EmptyProperty;\n };\n\n // get or create style, crawls up node tree\n this.style = function (name, createIfNotExists, skipAncestors) {\n let s = this.styles[name];\n if (s != null) return s;\n\n const a = this.attribute(name);\n if (a != null && a.hasValue()) {\n this.styles[name] = a; // move up to me to cache\n return a;\n }\n\n if (skipAncestors !== true) {\n const p = this.parent;\n if (p != null) {\n const ps = p.style(name);\n if (ps != null && ps.hasValue()) {\n return ps;\n }\n }\n }\n\n if (createIfNotExists === true) { s = new svg.Property(name, ''); this.styles[name] = s; }\n return s || svg.EmptyProperty;\n };\n\n // base render\n this.render = function (ctx) {\n // don't render display=none\n if (this.style('display').value === 'none') return;\n\n // don't render visibility=hidden\n if (this.style('visibility').value === 'hidden') return;\n\n ctx.save();\n if (this.attribute('mask').hasValue()) { // mask\n const mask = this.attribute('mask').getDefinition();\n if (mask != null) mask.apply(ctx, this);\n } else if (this.style('filter').hasValue()) { // filter\n const filter = this.style('filter').getDefinition();\n if (filter != null) filter.apply(ctx, this);\n } else {\n this.setContext(ctx);\n this.renderChildren(ctx);\n this.clearContext(ctx);\n }\n ctx.restore();\n };\n\n // base set context\n this.setContext = function (ctx) {\n // OVERRIDE ME!\n };\n\n // base clear context\n this.clearContext = function (ctx) {\n // OVERRIDE ME!\n };\n\n // base render children\n this.renderChildren = function (ctx) {\n for (let i = 0; i < this.children.length; i++) {\n this.children[i].render(ctx);\n }\n };\n\n this.addChild = function (childNode, create) {\n const child = create\n ? svg.CreateElement(childNode)\n : childNode;\n child.parent = this;\n if (child.type !== 'title') { this.children.push(child); }\n };\n\n if (node != null && node.nodeType === 1) { // ELEMENT_NODE\n // add children\n for (let i = 0, childNode; (childNode = node.childNodes[i]); i++) {\n if (childNode.nodeType === 1) this.addChild(childNode, true); // ELEMENT_NODE\n if (this.captureTextNodes && (childNode.nodeType === 3 || childNode.nodeType === 4)) {\n const text = childNode.nodeValue || childNode.text || '';\n if (svg.trim(svg.compressSpaces(text)) !== '') {\n this.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE\n }\n }\n }\n\n // add attributes\n for (let i = 0; i < node.attributes.length; i++) {\n const attribute = node.attributes[i];\n this.attributes[attribute.nodeName] = new svg.Property(attribute.nodeName, attribute.nodeValue);\n }\n\n // add tag styles\n let styles = svg.Styles[node.nodeName];\n if (styles != null) {\n for (const name in styles) {\n this.styles[name] = styles[name];\n }\n }\n\n // add class styles\n if (this.attribute('class').hasValue()) {\n const classes = svg.compressSpaces(this.attribute('class').value).split(' ');\n for (let j = 0; j < classes.length; j++) {\n styles = svg.Styles['.' + classes[j]];\n if (styles != null) {\n for (const name in styles) {\n this.styles[name] = styles[name];\n }\n }\n styles = svg.Styles[node.nodeName + '.' + classes[j]];\n if (styles != null) {\n for (const name in styles) {\n this.styles[name] = styles[name];\n }\n }\n }\n }\n\n // add id styles\n if (this.attribute('id').hasValue()) {\n const styles = svg.Styles['#' + this.attribute('id').value];\n if (styles != null) {\n for (const name in styles) {\n this.styles[name] = styles[name];\n }\n }\n }\n\n // add inline styles\n if (this.attribute('style').hasValue()) {\n const styles = this.attribute('style').value.split(';');\n for (let i = 0; i < styles.length; i++) {\n if (svg.trim(styles[i]) !== '') {\n const style = styles[i].split(':');\n const name = svg.trim(style[0]);\n const value = svg.trim(style[1]);\n this.styles[name] = new svg.Property(name, value);\n }\n }\n }\n\n // add id\n if (this.attribute('id').hasValue()) {\n if (svg.Definitions[this.attribute('id').value] == null) {\n svg.Definitions[this.attribute('id').value] = this;\n }\n }\n }\n };\n\n svg.Element.RenderedElementBase = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.setContext = function (ctx) {\n // fill\n if (this.style('fill').isUrlDefinition()) {\n const fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity'));\n if (fs != null) ctx.fillStyle = fs;\n } else if (this.style('fill').hasValue()) {\n const fillStyle = this.style('fill');\n if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value;\n ctx.fillStyle = (fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value);\n }\n if (this.style('fill-opacity').hasValue()) {\n let fillStyle = new svg.Property('fill', ctx.fillStyle);\n fillStyle = fillStyle.addOpacity(this.style('fill-opacity'));\n ctx.fillStyle = fillStyle.value;\n }\n\n // stroke\n if (this.style('stroke').isUrlDefinition()) {\n const fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity'));\n if (fs != null) ctx.strokeStyle = fs;\n } else if (this.style('stroke').hasValue()) {\n const strokeStyle = this.style('stroke');\n if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value;\n ctx.strokeStyle = (strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value);\n }\n if (this.style('stroke-opacity').hasValue()) {\n let strokeStyle = new svg.Property('stroke', ctx.strokeStyle);\n strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity'));\n ctx.strokeStyle = strokeStyle.value;\n }\n if (this.style('stroke-width').hasValue()) {\n const newLineWidth = this.style('stroke-width').toPixels();\n ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0\n }\n if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value;\n if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value;\n if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value;\n if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') {\n const gaps = svg.ToNumberArray(this.style('stroke-dasharray').value);\n if (typeof ctx.setLineDash !== 'undefined') {\n ctx.setLineDash(gaps);\n } else if (typeof ctx.webkitLineDash !== 'undefined') {\n ctx.webkitLineDash = gaps;\n } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) {\n ctx.mozDash = gaps;\n }\n\n const offset = this.style('stroke-dashoffset').numValueOrDefault(1);\n if (typeof ctx.lineDashOffset !== 'undefined') {\n ctx.lineDashOffset = offset;\n } else if (typeof ctx.webkitLineDashOffset !== 'undefined') {\n ctx.webkitLineDashOffset = offset;\n } else if (typeof ctx.mozDashOffset !== 'undefined') {\n ctx.mozDashOffset = offset;\n }\n }\n\n // font\n if (typeof ctx.font !== 'undefined') {\n ctx.font = svg.Font.CreateFont(\n this.style('font-style').value,\n this.style('font-variant').value,\n this.style('font-weight').value,\n this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '',\n this.style('font-family').value).toString();\n }\n\n // transform\n if (this.attribute('transform').hasValue()) {\n const transform = new svg.Transform(this.attribute('transform').value);\n transform.apply(ctx);\n }\n\n // clip\n if (this.style('clip-path', false, true).hasValue()) {\n const clip = this.style('clip-path', false, true).getDefinition();\n if (clip != null) clip.apply(ctx);\n }\n\n // opacity\n if (this.style('opacity').hasValue()) {\n ctx.globalAlpha = this.style('opacity').numValue();\n }\n };\n }\n };\n\n svg.Element.PathElementBase = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n\n this.path = function (ctx) {\n if (ctx != null) ctx.beginPath();\n return new svg.BoundingBox();\n };\n\n this.renderChildren = function (ctx) {\n this.path(ctx);\n svg.Mouse.checkPath(this, ctx);\n if (ctx.fillStyle !== '') {\n if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') {\n ctx.fill(this.style('fill-rule').value);\n } else {\n ctx.fill();\n }\n }\n if (ctx.strokeStyle !== '') ctx.stroke();\n\n const markers = this.getMarkers();\n if (markers != null) {\n if (this.style('marker-start').isUrlDefinition()) {\n const marker = this.style('marker-start').getDefinition();\n marker.render(ctx, markers[0][0], markers[0][1]);\n }\n if (this.style('marker-mid').isUrlDefinition()) {\n const marker = this.style('marker-mid').getDefinition();\n for (let i = 1; i < markers.length - 1; i++) {\n marker.render(ctx, markers[i][0], markers[i][1]);\n }\n }\n if (this.style('marker-end').isUrlDefinition()) {\n const marker = this.style('marker-end').getDefinition();\n marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]);\n }\n }\n };\n\n this.getBoundingBox = function () {\n return this.path();\n };\n\n this.getMarkers = function () {\n return null;\n };\n }\n };\n\n // svg element\n svg.Element.svg = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n\n this.baseClearContext = this.clearContext;\n this.clearContext = function (ctx) {\n this.baseClearContext(ctx);\n svg.ViewPort.RemoveCurrent();\n };\n\n this.baseSetContext = this.setContext;\n this.setContext = function (ctx) {\n // initial values and defaults\n ctx.strokeStyle = 'rgba(0,0,0,0)';\n ctx.lineCap = 'butt';\n ctx.lineJoin = 'miter';\n ctx.miterLimit = 4;\n if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') {\n ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font');\n }\n\n this.baseSetContext(ctx);\n\n // create new view port\n if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0;\n if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0;\n ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y'));\n\n let width = svg.ViewPort.width();\n let height = svg.ViewPort.height();\n\n if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%';\n if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%';\n if (typeof this.root === 'undefined') {\n width = this.attribute('width').toPixels('x');\n height = this.attribute('height').toPixels('y');\n\n let x = 0;\n let y = 0;\n if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) {\n x = -this.attribute('refX').toPixels('x');\n y = -this.attribute('refY').toPixels('y');\n }\n\n if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') {\n ctx.beginPath();\n ctx.moveTo(x, y);\n ctx.lineTo(width, y);\n ctx.lineTo(width, height);\n ctx.lineTo(x, height);\n ctx.closePath();\n ctx.clip();\n }\n }\n svg.ViewPort.SetCurrent(width, height);\n\n // viewbox\n if (this.attribute('viewBox').hasValue()) {\n const viewBox = svg.ToNumberArray(this.attribute('viewBox').value);\n const minX = viewBox[0];\n const minY = viewBox[1];\n width = viewBox[2];\n height = viewBox[3];\n\n svg.AspectRatio(\n ctx,\n this.attribute('preserveAspectRatio').value,\n svg.ViewPort.width(),\n width,\n svg.ViewPort.height(),\n height,\n minX,\n minY,\n this.attribute('refX').value,\n this.attribute('refY').value\n );\n\n svg.ViewPort.RemoveCurrent();\n svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]);\n }\n };\n }\n };\n\n // rect element\n svg.Element.rect = class extends svg.Element.PathElementBase {\n constructor (node) {\n super(node);\n\n this.path = function (ctx) {\n const x = this.attribute('x').toPixels('x');\n const y = this.attribute('y').toPixels('y');\n const width = this.attribute('width').toPixels('x');\n const height = this.attribute('height').toPixels('y');\n let rx = this.attribute('rx').toPixels('x');\n let ry = this.attribute('ry').toPixels('y');\n if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx;\n if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry;\n rx = Math.min(rx, width / 2.0);\n ry = Math.min(ry, height / 2.0);\n if (ctx != null) {\n ctx.beginPath();\n ctx.moveTo(x + rx, y);\n ctx.lineTo(x + width - rx, y);\n ctx.quadraticCurveTo(x + width, y, x + width, y + ry);\n ctx.lineTo(x + width, y + height - ry);\n ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height);\n ctx.lineTo(x + rx, y + height);\n ctx.quadraticCurveTo(x, y + height, x, y + height - ry);\n ctx.lineTo(x, y + ry);\n ctx.quadraticCurveTo(x, y, x + rx, y);\n ctx.closePath();\n }\n\n return new svg.BoundingBox(x, y, x + width, y + height);\n };\n }\n };\n\n // circle element\n svg.Element.circle = class extends svg.Element.PathElementBase {\n constructor (node) {\n super(node);\n\n this.path = function (ctx) {\n const cx = this.attribute('cx').toPixels('x');\n const cy = this.attribute('cy').toPixels('y');\n const r = this.attribute('r').toPixels();\n\n if (ctx != null) {\n ctx.beginPath();\n ctx.arc(cx, cy, r, 0, Math.PI * 2, true);\n ctx.closePath();\n }\n\n return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r);\n };\n }\n };\n\n // ellipse element\n svg.Element.ellipse = class extends svg.Element.PathElementBase {\n constructor (node) {\n super(node);\n\n this.path = function (ctx) {\n const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);\n const rx = this.attribute('rx').toPixels('x');\n const ry = this.attribute('ry').toPixels('y');\n const cx = this.attribute('cx').toPixels('x');\n const cy = this.attribute('cy').toPixels('y');\n\n if (ctx != null) {\n ctx.beginPath();\n ctx.moveTo(cx, cy - ry);\n ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy);\n ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry);\n ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy);\n ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry);\n ctx.closePath();\n }\n\n return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry);\n };\n }\n };\n\n // line element\n svg.Element.line = class extends svg.Element.PathElementBase {\n constructor (node) {\n super(node);\n\n this.getPoints = function () {\n return [\n new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')),\n new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y'))];\n };\n\n this.path = function (ctx) {\n const points = this.getPoints();\n\n if (ctx != null) {\n ctx.beginPath();\n ctx.moveTo(points[0].x, points[0].y);\n ctx.lineTo(points[1].x, points[1].y);\n }\n\n return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y);\n };\n\n this.getMarkers = function () {\n const points = this.getPoints();\n const a = points[0].angleTo(points[1]);\n return [[points[0], a], [points[1], a]];\n };\n }\n };\n\n // polyline element\n svg.Element.polyline = class extends svg.Element.PathElementBase {\n constructor (node) {\n super(node);\n\n this.points = svg.CreatePath(this.attribute('points').value);\n this.path = function (ctx) {\n const bb = new svg.BoundingBox(this.points[0].x, this.points[0].y);\n if (ctx != null) {\n ctx.beginPath();\n ctx.moveTo(this.points[0].x, this.points[0].y);\n }\n for (let i = 1; i < this.points.length; i++) {\n bb.addPoint(this.points[i].x, this.points[i].y);\n if (ctx != null) ctx.lineTo(this.points[i].x, this.points[i].y);\n }\n return bb;\n };\n\n this.getMarkers = function () {\n const markers = [];\n for (let i = 0; i < this.points.length - 1; i++) {\n markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]);\n }\n markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]);\n return markers;\n };\n }\n };\n\n // polygon element\n svg.Element.polygon = class extends svg.Element.polyline {\n constructor (node) {\n super(node);\n\n this.basePath = this.path;\n this.path = function (ctx) {\n const bb = this.basePath(ctx);\n if (ctx != null) {\n ctx.lineTo(this.points[0].x, this.points[0].y);\n ctx.closePath();\n }\n return bb;\n };\n }\n };\n\n // path element\n svg.Element.path = class extends svg.Element.PathElementBase {\n constructor (node) {\n super(node);\n\n let d = this.attribute('d').value;\n // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF\n d = d.replace(/,/gm, ' '); // get rid of all commas\n d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands\n d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands\n d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([^\\s])/gm, '$1 $2'); // separate commands from points\n d = d.replace(/([^\\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from points\n d = d.replace(/([0-9])([+-])/gm, '$1 $2'); // separate digits when no comma\n d = d.replace(/(\\.[0-9]*)(\\.)/gm, '$1 $2'); // separate digits when no comma\n d = d.replace(/([Aa](\\s+[0-9]+){3})\\s+([01])\\s*([01])/gm, '$1 $3 $4 '); // shorthand elliptical arc path syntax\n d = svg.compressSpaces(d); // compress multiple spaces\n d = svg.trim(d);\n this.PathParser = new function (d) {\n this.tokens = d.split(' ');\n\n this.reset = function () {\n this.i = -1;\n this.command = '';\n this.previousCommand = '';\n this.start = new svg.Point(0, 0);\n this.control = new svg.Point(0, 0);\n this.current = new svg.Point(0, 0);\n this.points = [];\n this.angles = [];\n };\n\n this.isEnd = function () {\n return this.i >= this.tokens.length - 1;\n };\n\n this.isCommandOrEnd = function () {\n if (this.isEnd()) return true;\n return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null;\n };\n\n this.isRelativeCommand = function () {\n switch (this.command) {\n case 'm':\n case 'l':\n case 'h':\n case 'v':\n case 'c':\n case 's':\n case 'q':\n case 't':\n case 'a':\n case 'z':\n return true;\n }\n return false;\n };\n\n this.getToken = function () {\n this.i++;\n return this.tokens[this.i];\n };\n\n this.getScalar = function () {\n return parseFloat(this.getToken());\n };\n\n this.nextCommand = function () {\n this.previousCommand = this.command;\n this.command = this.getToken();\n };\n\n this.getPoint = function () {\n const p = new svg.Point(this.getScalar(), this.getScalar());\n return this.makeAbsolute(p);\n };\n\n this.getAsControlPoint = function () {\n const p = this.getPoint();\n this.control = p;\n return p;\n };\n\n this.getAsCurrentPoint = function () {\n const p = this.getPoint();\n this.current = p;\n return p;\n };\n\n this.getReflectedControlPoint = function () {\n if (this.previousCommand.toLowerCase() !== 'c' &&\n this.previousCommand.toLowerCase() !== 's' &&\n this.previousCommand.toLowerCase() !== 'q' &&\n this.previousCommand.toLowerCase() !== 't') {\n return this.current;\n }\n\n // reflect point\n const p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y);\n return p;\n };\n\n this.makeAbsolute = function (p) {\n if (this.isRelativeCommand()) {\n p.x += this.current.x;\n p.y += this.current.y;\n }\n return p;\n };\n\n this.addMarker = function (p, from, priorTo) {\n // if the last angle isn't filled in because we didn't have this point yet ...\n if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length - 1] == null) {\n this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo);\n }\n this.addMarkerAngle(p, from == null ? null : from.angleTo(p));\n };\n\n this.addMarkerAngle = function (p, a) {\n this.points.push(p);\n this.angles.push(a);\n };\n\n this.getMarkerPoints = function () { return this.points; };\n this.getMarkerAngles = function () {\n for (let i = 0; i < this.angles.length; i++) {\n if (this.angles[i] == null) {\n for (let j = i + 1; j < this.angles.length; j++) {\n if (this.angles[j] != null) {\n this.angles[i] = this.angles[j];\n break;\n }\n }\n }\n }\n return this.angles;\n };\n }(d);\n\n this.path = function (ctx) {\n const pp = this.PathParser;\n pp.reset();\n\n const bb = new svg.BoundingBox();\n if (ctx != null) ctx.beginPath();\n while (!pp.isEnd()) {\n pp.nextCommand();\n switch (pp.command) {\n case 'M':\n case 'm':\n const p = pp.getAsCurrentPoint();\n pp.addMarker(p);\n bb.addPoint(p.x, p.y);\n if (ctx != null) ctx.moveTo(p.x, p.y);\n pp.start = pp.current;\n while (!pp.isCommandOrEnd()) {\n const p = pp.getAsCurrentPoint();\n pp.addMarker(p, pp.start);\n bb.addPoint(p.x, p.y);\n if (ctx != null) ctx.lineTo(p.x, p.y);\n }\n break;\n case 'L':\n case 'l':\n while (!pp.isCommandOrEnd()) {\n const c = pp.current;\n const p = pp.getAsCurrentPoint();\n pp.addMarker(p, c);\n bb.addPoint(p.x, p.y);\n if (ctx != null) ctx.lineTo(p.x, p.y);\n }\n break;\n case 'H':\n case 'h':\n while (!pp.isCommandOrEnd()) {\n const newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y);\n pp.addMarker(newP, pp.current);\n pp.current = newP;\n bb.addPoint(pp.current.x, pp.current.y);\n if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y);\n }\n break;\n case 'V':\n case 'v':\n while (!pp.isCommandOrEnd()) {\n const newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar());\n pp.addMarker(newP, pp.current);\n pp.current = newP;\n bb.addPoint(pp.current.x, pp.current.y);\n if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y);\n }\n break;\n case 'C':\n case 'c':\n while (!pp.isCommandOrEnd()) {\n const curr = pp.current;\n const p1 = pp.getPoint();\n const cntrl = pp.getAsControlPoint();\n const cp = pp.getAsCurrentPoint();\n pp.addMarker(cp, cntrl, p1);\n bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);\n if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);\n }\n break;\n case 'S':\n case 's':\n while (!pp.isCommandOrEnd()) {\n const curr = pp.current;\n const p1 = pp.getReflectedControlPoint();\n const cntrl = pp.getAsControlPoint();\n const cp = pp.getAsCurrentPoint();\n pp.addMarker(cp, cntrl, p1);\n bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);\n if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);\n }\n break;\n case 'Q':\n case 'q':\n while (!pp.isCommandOrEnd()) {\n const curr = pp.current;\n const cntrl = pp.getAsControlPoint();\n const cp = pp.getAsCurrentPoint();\n pp.addMarker(cp, cntrl, cntrl);\n bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y);\n if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y);\n }\n break;\n case 'T':\n case 't':\n while (!pp.isCommandOrEnd()) {\n const curr = pp.current;\n const cntrl = pp.getReflectedControlPoint();\n pp.control = cntrl;\n const cp = pp.getAsCurrentPoint();\n pp.addMarker(cp, cntrl, cntrl);\n bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y);\n if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y);\n }\n break;\n case 'A':\n case 'a':\n while (!pp.isCommandOrEnd()) {\n const curr = pp.current;\n let rx = pp.getScalar();\n let ry = pp.getScalar();\n const xAxisRotation = pp.getScalar() * (Math.PI / 180.0);\n const largeArcFlag = pp.getScalar();\n const sweepFlag = pp.getScalar();\n const cp = pp.getAsCurrentPoint();\n\n // Conversion from endpoint to center parameterization\n // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter\n\n // x1', y1'\n const currp = new svg.Point(\n Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0,\n -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0\n );\n // adjust radii\n const l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2);\n if (l > 1) {\n rx *= Math.sqrt(l);\n ry *= Math.sqrt(l);\n }\n // cx', cy'\n let s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt(\n ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(currp.y, 2)) - (Math.pow(ry, 2) * Math.pow(currp.x, 2))) /\n (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2))\n );\n if (isNaN(s)) s = 0;\n const cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx);\n // cx, cy\n const centp = new svg.Point(\n (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y,\n (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y\n );\n // vector magnitude\n const m = function (v) { return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); };\n // ratio between two vectors\n const r = function (u, v) { return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); };\n // angle between two vectors\n const a = function (u, v) { return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); };\n // initial angle\n const a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]);\n // angle delta\n const u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry];\n const v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry];\n let ad = a(u, v);\n if (r(u, v) <= -1) ad = Math.PI;\n if (r(u, v) >= 1) ad = 0;\n\n // for markers\n const dir = 1 - sweepFlag ? 1.0 : -1.0;\n const ah = a1 + dir * (ad / 2.0);\n const halfWay = new svg.Point(\n centp.x + rx * Math.cos(ah),\n centp.y + ry * Math.sin(ah)\n );\n pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2);\n pp.addMarkerAngle(cp, ah - dir * Math.PI);\n\n bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better\n if (ctx != null) {\n const r = rx > ry ? rx : ry;\n const sx = rx > ry ? 1 : rx / ry;\n const sy = rx > ry ? ry / rx : 1;\n\n ctx.translate(centp.x, centp.y);\n ctx.rotate(xAxisRotation);\n ctx.scale(sx, sy);\n ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag);\n ctx.scale(1 / sx, 1 / sy);\n ctx.rotate(-xAxisRotation);\n ctx.translate(-centp.x, -centp.y);\n }\n }\n break;\n case 'Z':\n case 'z':\n if (ctx != null) ctx.closePath();\n pp.current = pp.start;\n }\n }\n\n return bb;\n };\n\n this.getMarkers = function () {\n const points = this.PathParser.getMarkerPoints();\n const angles = this.PathParser.getMarkerAngles();\n\n const markers = [];\n for (let i = 0; i < points.length; i++) {\n markers.push([points[i], angles[i]]);\n }\n return markers;\n };\n }\n };\n\n // pattern element\n svg.Element.pattern = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.createPattern = function (ctx, element) {\n const width = this.attribute('width').toPixels('x', true);\n const height = this.attribute('height').toPixels('y', true);\n\n // render me using a temporary svg element\n const tempSvg = new svg.Element.svg();\n tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value);\n tempSvg.attributes['width'] = new svg.Property('width', width + 'px');\n tempSvg.attributes['height'] = new svg.Property('height', height + 'px');\n tempSvg.attributes['transform'] = new svg.Property('transform', this.attribute('patternTransform').value);\n tempSvg.children = this.children;\n\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n const cctx = c.getContext('2d');\n if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) {\n cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true));\n }\n // render 3x3 grid so when we transform there's no white space on edges\n for (let x = -1; x <= 1; x++) {\n for (let y = -1; y <= 1; y++) {\n cctx.save();\n cctx.translate(x * c.width, y * c.height);\n tempSvg.render(cctx);\n cctx.restore();\n }\n }\n const pattern = ctx.createPattern(c, 'repeat');\n return pattern;\n };\n }\n };\n\n // marker element\n svg.Element.marker = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.baseRender = this.render;\n this.render = function (ctx, point, angle) {\n ctx.translate(point.x, point.y);\n if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle);\n if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth);\n ctx.save();\n\n // render me using a temporary svg element\n const tempSvg = new svg.Element.svg();\n tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value);\n tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value);\n tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value);\n tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value);\n tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value);\n tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black'));\n tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none'));\n tempSvg.children = this.children;\n tempSvg.render(ctx);\n\n ctx.restore();\n if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth);\n if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(-angle);\n ctx.translate(-point.x, -point.y);\n };\n }\n };\n\n // definitions element\n svg.Element.defs = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.render = function (ctx) {\n // NOOP\n };\n }\n };\n\n // base for gradients\n svg.Element.GradientBase = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox');\n\n this.stops = [];\n for (let i = 0; i < this.children.length; i++) {\n const child = this.children[i];\n if (child.type === 'stop') this.stops.push(child);\n }\n\n this.getGradient = function () {\n // OVERRIDE ME!\n };\n\n this.createGradient = function (ctx, element, parentOpacityProp) {\n const stopsContainer = this.getHrefAttribute().hasValue()\n ? this.getHrefAttribute().getDefinition()\n : this;\n\n const addParentOpacity = function (color) {\n if (parentOpacityProp.hasValue()) {\n const p = new svg.Property('color', color);\n return p.addOpacity(parentOpacityProp).value;\n }\n return color;\n };\n\n const g = this.getGradient(ctx, element);\n if (g == null) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color);\n for (let i = 0; i < stopsContainer.stops.length; i++) {\n g.addColorStop(stopsContainer.stops[i].offset, addParentOpacity(stopsContainer.stops[i].color));\n }\n\n if (this.attribute('gradientTransform').hasValue()) {\n // render as transformed pattern on temporary canvas\n const rootView = svg.ViewPort.viewPorts[0];\n\n const rect = new svg.Element.rect();\n rect.attributes['x'] = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0);\n rect.attributes['y'] = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0);\n rect.attributes['width'] = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS);\n rect.attributes['height'] = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS);\n\n const group = new svg.Element.g();\n group.attributes['transform'] = new svg.Property('transform', this.attribute('gradientTransform').value);\n group.children = [ rect ];\n\n const tempSvg = new svg.Element.svg();\n tempSvg.attributes['x'] = new svg.Property('x', 0);\n tempSvg.attributes['y'] = new svg.Property('y', 0);\n tempSvg.attributes['width'] = new svg.Property('width', rootView.width);\n tempSvg.attributes['height'] = new svg.Property('height', rootView.height);\n tempSvg.children = [ group ];\n\n const c = document.createElement('canvas');\n c.width = rootView.width;\n c.height = rootView.height;\n const tempCtx = c.getContext('2d');\n tempCtx.fillStyle = g;\n tempSvg.render(tempCtx);\n return tempCtx.createPattern(c, 'no-repeat');\n }\n\n return g;\n };\n }\n };\n\n // linear gradient element\n svg.Element.linearGradient = class extends svg.Element.GradientBase {\n constructor (node) {\n super(node);\n\n this.getGradient = function (ctx, element) {\n const bb = this.gradientUnits === 'objectBoundingBox'\n ? element.getBoundingBox()\n : null;\n\n if (!this.attribute('x1').hasValue() &&\n !this.attribute('y1').hasValue() &&\n !this.attribute('x2').hasValue() &&\n !this.attribute('y2').hasValue()\n ) {\n this.attribute('x1', true).value = 0;\n this.attribute('y1', true).value = 0;\n this.attribute('x2', true).value = 1;\n this.attribute('y2', true).value = 0;\n }\n\n const x1 = (this.gradientUnits === 'objectBoundingBox'\n ? bb.x() + bb.width() * this.attribute('x1').numValue()\n : this.attribute('x1').toPixels('x'));\n const y1 = (this.gradientUnits === 'objectBoundingBox'\n ? bb.y() + bb.height() * this.attribute('y1').numValue()\n : this.attribute('y1').toPixels('y'));\n const x2 = (this.gradientUnits === 'objectBoundingBox'\n ? bb.x() + bb.width() * this.attribute('x2').numValue()\n : this.attribute('x2').toPixels('x'));\n const y2 = (this.gradientUnits === 'objectBoundingBox'\n ? bb.y() + bb.height() * this.attribute('y2').numValue()\n : this.attribute('y2').toPixels('y'));\n\n if (x1 === x2 && y1 === y2) return null;\n return ctx.createLinearGradient(x1, y1, x2, y2);\n };\n }\n };\n\n // radial gradient element\n svg.Element.radialGradient = class extends svg.Element.GradientBase {\n constructor (node) {\n super(node);\n\n this.getGradient = function (ctx, element) {\n const bb = element.getBoundingBox();\n\n if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%';\n if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%';\n if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%';\n\n const cx = (this.gradientUnits === 'objectBoundingBox'\n ? bb.x() + bb.width() * this.attribute('cx').numValue()\n : this.attribute('cx').toPixels('x'));\n const cy = (this.gradientUnits === 'objectBoundingBox'\n ? bb.y() + bb.height() * this.attribute('cy').numValue()\n : this.attribute('cy').toPixels('y'));\n\n let fx = cx;\n let fy = cy;\n if (this.attribute('fx').hasValue()) {\n fx = (this.gradientUnits === 'objectBoundingBox'\n ? bb.x() + bb.width() * this.attribute('fx').numValue()\n : this.attribute('fx').toPixels('x'));\n }\n if (this.attribute('fy').hasValue()) {\n fy = (this.gradientUnits === 'objectBoundingBox'\n ? bb.y() + bb.height() * this.attribute('fy').numValue()\n : this.attribute('fy').toPixels('y'));\n }\n\n const r = (this.gradientUnits === 'objectBoundingBox'\n ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue()\n : this.attribute('r').toPixels());\n\n return ctx.createRadialGradient(fx, fy, 0, cx, cy, r);\n };\n }\n };\n\n // gradient stop element\n svg.Element.stop = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.offset = this.attribute('offset').numValue();\n if (this.offset < 0) this.offset = 0;\n if (this.offset > 1) this.offset = 1;\n\n let stopColor = this.style('stop-color');\n if (this.style('stop-opacity').hasValue()) {\n stopColor = stopColor.addOpacity(this.style('stop-opacity'));\n }\n this.color = stopColor.value;\n }\n };\n\n // animation base element\n svg.Element.AnimateBase = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n svg.Animations.push(this);\n\n this.duration = 0.0;\n this.begin = this.attribute('begin').toMilliseconds();\n this.maxDuration = this.begin + this.attribute('dur').toMilliseconds();\n\n this.getProperty = function () {\n const attributeType = this.attribute('attributeType').value;\n const attributeName = this.attribute('attributeName').value;\n\n if (attributeType === 'CSS') {\n return this.parent.style(attributeName, true);\n }\n return this.parent.attribute(attributeName, true);\n };\n\n this.initialValue = null;\n this.initialUnits = '';\n this.removed = false;\n\n this.calcValue = function () {\n // OVERRIDE ME!\n return '';\n };\n\n this.update = function (delta) {\n // set initial value\n if (this.initialValue == null) {\n this.initialValue = this.getProperty().value;\n this.initialUnits = this.getProperty().getUnits();\n }\n\n // if we're past the end time\n if (this.duration > this.maxDuration) {\n // loop for indefinitely repeating animations\n if (this.attribute('repeatCount').value === 'indefinite' ||\n this.attribute('repeatDur').value === 'indefinite') {\n this.duration = 0.0;\n } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) {\n this.frozen = true;\n this.parent.animationFrozen = true;\n this.parent.animationFrozenValue = this.getProperty().value;\n } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) {\n this.removed = true;\n this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue;\n return true;\n }\n return false;\n }\n this.duration = this.duration + delta;\n\n // if we're past the begin time\n let updated = false;\n if (this.begin < this.duration) {\n let newValue = this.calcValue(); // tween\n\n if (this.attribute('type').hasValue()) {\n // for transform, etc.\n const type = this.attribute('type').value;\n newValue = type + '(' + newValue + ')';\n }\n\n this.getProperty().value = newValue;\n updated = true;\n }\n\n return updated;\n };\n\n this.from = this.attribute('from');\n this.to = this.attribute('to');\n this.values = this.attribute('values');\n if (this.values.hasValue()) this.values.value = this.values.value.split(';');\n\n // fraction of duration we've covered\n this.progress = function () {\n const ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) };\n if (this.values.hasValue()) {\n const p = ret.progress * (this.values.value.length - 1);\n const lb = Math.floor(p), ub = Math.ceil(p);\n ret.from = new svg.Property('from', parseFloat(this.values.value[lb]));\n ret.to = new svg.Property('to', parseFloat(this.values.value[ub]));\n ret.progress = (p - lb) / (ub - lb);\n } else {\n ret.from = this.from;\n ret.to = this.to;\n }\n return ret;\n };\n }\n };\n\n // animate element\n svg.Element.animate = class extends svg.Element.AnimateBase {\n constructor (node) {\n super(node);\n\n this.calcValue = function () {\n const p = this.progress();\n\n // tween value linearly\n const newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress;\n return newValue + this.initialUnits;\n };\n }\n };\n\n // animate color element\n svg.Element.animateColor = class extends svg.Element.AnimateBase {\n constructor (node) {\n super(node);\n\n this.calcValue = function () {\n const p = this.progress();\n const from = new RGBColor(p.from.value);\n const to = new RGBColor(p.to.value);\n\n if (from.ok && to.ok) {\n // tween color linearly\n const r = from.r + (to.r - from.r) * p.progress;\n const g = from.g + (to.g - from.g) * p.progress;\n const b = from.b + (to.b - from.b) * p.progress;\n return 'rgb(' + parseInt(r, 10) + ',' + parseInt(g, 10) + ',' + parseInt(b, 10) + ')';\n }\n return this.attribute('from').value;\n };\n }\n };\n\n // animate transform element\n svg.Element.animateTransform = class extends svg.Element.animate {\n constructor (node) {\n super(node);\n\n this.calcValue = function () {\n const p = this.progress();\n\n // tween value linearly\n const from = svg.ToNumberArray(p.from.value);\n const to = svg.ToNumberArray(p.to.value);\n let newValue = '';\n for (let i = 0; i < from.length; i++) {\n newValue += from[i] + (to[i] - from[i]) * p.progress + ' ';\n }\n return newValue;\n };\n }\n };\n\n // font element\n svg.Element.font = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.horizAdvX = this.attribute('horiz-adv-x').numValue();\n\n this.isRTL = false;\n this.isArabic = false;\n this.fontFace = null;\n this.missingGlyph = null;\n this.glyphs = [];\n for (let i = 0; i < this.children.length; i++) {\n const child = this.children[i];\n if (child.type === 'font-face') {\n this.fontFace = child;\n if (child.style('font-family').hasValue()) {\n svg.Definitions[child.style('font-family').value] = this;\n }\n } else if (child.type === 'missing-glyph') {\n this.missingGlyph = child;\n } else if (child.type === 'glyph') {\n if (child.arabicForm !== '') {\n this.isRTL = true;\n this.isArabic = true;\n if (typeof this.glyphs[child.unicode] === 'undefined') {\n this.glyphs[child.unicode] = [];\n }\n this.glyphs[child.unicode][child.arabicForm] = child;\n } else {\n this.glyphs[child.unicode] = child;\n }\n }\n }\n }\n };\n\n // font-face element\n svg.Element.fontface = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.ascent = this.attribute('ascent').value;\n this.descent = this.attribute('descent').value;\n this.unitsPerEm = this.attribute('units-per-em').numValue();\n }\n };\n\n // missing-glyph element\n svg.Element.missingglyph = class extends svg.Element.path {\n constructor (node) {\n super(node);\n\n this.horizAdvX = 0;\n }\n };\n\n // glyph element\n svg.Element.glyph = class extends svg.Element.path {\n constructor (node) {\n super(node);\n\n this.horizAdvX = this.attribute('horiz-adv-x').numValue();\n this.unicode = this.attribute('unicode').value;\n this.arabicForm = this.attribute('arabic-form').value;\n }\n };\n\n // text element\n svg.Element.text = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n this.captureTextNodes = true;\n\n this.baseSetContext = this.setContext;\n this.setContext = function (ctx) {\n this.baseSetContext(ctx);\n\n let textBaseline = this.style('dominant-baseline').toTextBaseline();\n if (textBaseline == null) textBaseline = this.style('alignment-baseline').toTextBaseline();\n if (textBaseline != null) ctx.textBaseline = textBaseline;\n };\n\n this.getBoundingBox = function () {\n const x = this.attribute('x').toPixels('x');\n const y = this.attribute('y').toPixels('y');\n const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize);\n return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y);\n };\n\n this.renderChildren = function (ctx) {\n this.x = this.attribute('x').toPixels('x');\n this.y = this.attribute('y').toPixels('y');\n this.x += this.getAnchorDelta(ctx, this, 0);\n for (let i = 0; i < this.children.length; i++) {\n this.renderChild(ctx, this, i);\n }\n };\n\n this.getAnchorDelta = function (ctx, parent, startI) {\n const textAnchor = this.style('text-anchor').valueOrDefault('start');\n if (textAnchor !== 'start') {\n let width = 0;\n for (let i = startI; i < parent.children.length; i++) {\n const child = parent.children[i];\n if (i > startI && child.attribute('x').hasValue()) break; // new group\n width += child.measureTextRecursive(ctx);\n }\n return -1 * (textAnchor === 'end' ? width : width / 2.0);\n }\n return 0;\n };\n\n this.renderChild = function (ctx, parent, i) {\n const child = parent.children[i];\n if (child.attribute('x').hasValue()) {\n child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i);\n if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x');\n } else {\n if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x');\n if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x');\n child.x = this.x;\n }\n this.x = child.x + child.measureText(ctx);\n\n if (child.attribute('y').hasValue()) {\n child.y = child.attribute('y').toPixels('y');\n if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y');\n } else {\n if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y');\n if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y');\n child.y = this.y;\n }\n this.y = child.y;\n\n child.render(ctx);\n\n for (let i = 0; i < child.children.length; i++) {\n this.renderChild(ctx, child, i);\n }\n };\n }\n };\n\n // text base\n svg.Element.TextElementBase = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n\n this.getGlyph = function (font, text, i) {\n const c = text[i];\n let glyph = null;\n if (font.isArabic) {\n let arabicForm = 'isolated';\n if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal';\n if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial';\n if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial';\n if (typeof font.glyphs[c] !== 'undefined') {\n glyph = font.glyphs[c][arabicForm];\n if (glyph == null && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c];\n }\n } else {\n glyph = font.glyphs[c];\n }\n if (glyph == null) glyph = font.missingGlyph;\n return glyph;\n };\n\n this.renderChildren = function (ctx) {\n const customFont = this.parent.style('font-family').getDefinition();\n if (customFont != null) {\n const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize);\n const fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle);\n let text = this.getText();\n if (customFont.isRTL) text = text.split('').reverse().join('');\n\n const dx = svg.ToNumberArray(this.parent.attribute('dx').value);\n for (let i = 0; i < text.length; i++) {\n const glyph = this.getGlyph(customFont, text, i);\n const scale = fontSize / customFont.fontFace.unitsPerEm;\n ctx.translate(this.x, this.y);\n ctx.scale(scale, -scale);\n const lw = ctx.lineWidth;\n ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize;\n if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0);\n glyph.render(ctx);\n if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0);\n ctx.lineWidth = lw;\n ctx.scale(1 / scale, -1 / scale);\n ctx.translate(-this.x, -this.y);\n\n this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm;\n if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {\n this.x += dx[i];\n }\n }\n return;\n }\n\n if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y);\n if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y);\n };\n\n this.getText = function () {\n // OVERRIDE ME\n };\n\n this.measureTextRecursive = function (ctx) {\n let width = this.measureText(ctx);\n for (let i = 0; i < this.children.length; i++) {\n width += this.children[i].measureTextRecursive(ctx);\n }\n return width;\n };\n\n this.measureText = function (ctx) {\n const customFont = this.parent.style('font-family').getDefinition();\n if (customFont != null) {\n const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize);\n let measure = 0;\n let text = this.getText();\n if (customFont.isRTL) text = text.split('').reverse().join('');\n const dx = svg.ToNumberArray(this.parent.attribute('dx').value);\n for (let i = 0; i < text.length; i++) {\n const glyph = this.getGlyph(customFont, text, i);\n measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm;\n if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) {\n measure += dx[i];\n }\n }\n return measure;\n }\n\n const textToMeasure = svg.compressSpaces(this.getText());\n if (!ctx.measureText) return textToMeasure.length * 10;\n\n ctx.save();\n this.setContext(ctx);\n const {width} = ctx.measureText(textToMeasure);\n ctx.restore();\n return width;\n };\n }\n };\n\n // tspan\n svg.Element.tspan = class extends svg.Element.TextElementBase {\n constructor (node) {\n super(node);\n this.captureTextNodes = true;\n\n this.text = node.nodeValue || node.text || '';\n this.getText = function () {\n return this.text;\n };\n }\n };\n\n // tref\n svg.Element.tref = class extends svg.Element.TextElementBase {\n constructor (node) {\n super(node);\n\n this.getText = function () {\n const element = this.getHrefAttribute().getDefinition();\n if (element != null) return element.children[0].getText();\n };\n }\n };\n\n // a element\n svg.Element.a = class extends svg.Element.TextElementBase {\n constructor (node) {\n super(node);\n\n this.hasText = true;\n for (let i = 0, childNode; (childNode = node.childNodes[i]); i++) {\n if (childNode.nodeType !== 3) this.hasText = false;\n }\n\n // this might contain text\n this.text = this.hasText ? node.childNodes[0].nodeValue : '';\n this.getText = function () {\n return this.text;\n };\n\n this.baseRenderChildren = this.renderChildren;\n this.renderChildren = function (ctx) {\n if (this.hasText) {\n // render as text element\n this.baseRenderChildren(ctx);\n const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize);\n svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y));\n } else {\n // render as temporary group\n const g = new svg.Element.g();\n g.children = this.children;\n g.parent = this;\n g.render(ctx);\n }\n };\n\n this.onclick = function () {\n window.open(this.getHrefAttribute().value);\n };\n\n this.onmousemove = function () {\n svg.ctx.canvas.style.cursor = 'pointer';\n };\n }\n };\n\n // image element\n svg.Element.image = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n\n const href = this.getHrefAttribute().value;\n if (href === '') {\n return;\n }\n const isSvg = href.match(/\\.svg$/);\n\n svg.Images.push(this);\n this.loaded = false;\n if (!isSvg) {\n this.img = document.createElement('img');\n if (svg.opts['useCORS'] === true) { this.img.crossOrigin = 'Anonymous'; }\n const self = this;\n this.img.onload = function () { self.loaded = true; };\n this.img.onerror = function () { svg.log('ERROR: image \"' + href + '\" not found'); self.loaded = true; };\n this.img.src = href;\n } else {\n this.img = svg.ajax(href);\n this.loaded = true;\n }\n\n this.renderChildren = function (ctx) {\n const x = this.attribute('x').toPixels('x');\n const y = this.attribute('y').toPixels('y');\n\n const width = this.attribute('width').toPixels('x');\n const height = this.attribute('height').toPixels('y');\n if (width === 0 || height === 0) return;\n\n ctx.save();\n if (isSvg) {\n ctx.drawSvg(this.img, x, y, width, height);\n } else {\n ctx.translate(x, y);\n svg.AspectRatio(\n ctx,\n this.attribute('preserveAspectRatio').value,\n width,\n this.img.width,\n height,\n this.img.height,\n 0,\n 0\n );\n ctx.drawImage(this.img, 0, 0);\n }\n ctx.restore();\n };\n\n this.getBoundingBox = function () {\n const x = this.attribute('x').toPixels('x');\n const y = this.attribute('y').toPixels('y');\n const width = this.attribute('width').toPixels('x');\n const height = this.attribute('height').toPixels('y');\n return new svg.BoundingBox(x, y, x + width, y + height);\n };\n }\n };\n\n // group element\n svg.Element.g = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n\n this.getBoundingBox = function () {\n const bb = new svg.BoundingBox();\n for (let i = 0; i < this.children.length; i++) {\n bb.addBoundingBox(this.children[i].getBoundingBox());\n }\n return bb;\n };\n }\n };\n\n // symbol element\n svg.Element.symbol = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n\n this.render = function (ctx) {\n // NO RENDER\n };\n }\n };\n\n // style element\n svg.Element.style = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n // text, or spaces then CDATA\n let css = '';\n for (let i = 0, childNode; (childNode = node.childNodes[i]); i++) {\n css += childNode.nodeValue;\n }\n css = css.replace(/(\\/\\*([^*]|[\\r\\n]|(\\*+([^*/]|[\\r\\n])))*\\*+\\/)|(^[\\s]*\\/\\/.*)/gm, ''); // remove comments\n css = svg.compressSpaces(css); // replace whitespace\n const cssDefs = css.split('}');\n for (let i = 0; i < cssDefs.length; i++) {\n if (svg.trim(cssDefs[i]) !== '') {\n const cssDef = cssDefs[i].split('{');\n const cssClasses = cssDef[0].split(',');\n const cssProps = cssDef[1].split(';');\n for (let j = 0; j < cssClasses.length; j++) {\n const cssClass = svg.trim(cssClasses[j]);\n if (cssClass !== '') {\n const props = {};\n for (let k = 0; k < cssProps.length; k++) {\n const prop = cssProps[k].indexOf(':');\n const name = cssProps[k].substr(0, prop);\n const value = cssProps[k].substr(prop + 1, cssProps[k].length - prop);\n if (name != null && value != null) {\n props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value));\n }\n }\n svg.Styles[cssClass] = props;\n if (cssClass === '@font-face') {\n const fontFamily = props['font-family'].value.replace(/\"/g, '');\n const srcs = props['src'].value.split(',');\n for (let s = 0; s < srcs.length; s++) {\n if (srcs[s].includes('format(\"svg\")')) {\n const urlStart = srcs[s].indexOf('url');\n const urlEnd = srcs[s].indexOf(')', urlStart);\n const url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6);\n const doc = svg.parseXml(svg.ajax(url));\n const fonts = doc.getElementsByTagName('font');\n for (let f = 0; f < fonts.length; f++) {\n const font = svg.CreateElement(fonts[f]);\n svg.Definitions[fontFamily] = font;\n }\n }\n }\n }\n }\n }\n }\n }\n }\n };\n\n // use element\n svg.Element.use = class extends svg.Element.RenderedElementBase {\n constructor (node) {\n super(node);\n\n this.baseSetContext = this.setContext;\n this.setContext = function (ctx) {\n this.baseSetContext(ctx);\n if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0);\n if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y'));\n };\n\n const element = this.getHrefAttribute().getDefinition();\n\n this.path = function (ctx) {\n if (element != null) element.path(ctx);\n };\n\n this.getBoundingBox = function () {\n if (element != null) return element.getBoundingBox();\n };\n\n this.renderChildren = function (ctx) {\n if (element != null) {\n let tempSvg = element;\n if (element.type === 'symbol') {\n // render me using a temporary svg element in symbol cases (https://www.w3.org/TR/SVG/struct.html#UseElement)\n tempSvg = new svg.Element.svg();\n tempSvg.type = 'svg';\n tempSvg.attributes['viewBox'] = new svg.Property('viewBox', element.attribute('viewBox').value);\n tempSvg.attributes['preserveAspectRatio'] = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value);\n tempSvg.attributes['overflow'] = new svg.Property('overflow', element.attribute('overflow').value);\n tempSvg.children = element.children;\n }\n if (tempSvg.type === 'svg') {\n // if symbol or svg, inherit width/height from me\n if (this.attribute('width').hasValue()) tempSvg.attributes['width'] = new svg.Property('width', this.attribute('width').value);\n if (this.attribute('height').hasValue()) tempSvg.attributes['height'] = new svg.Property('height', this.attribute('height').value);\n }\n const oldParent = tempSvg.parent;\n tempSvg.parent = null;\n tempSvg.render(ctx);\n tempSvg.parent = oldParent;\n }\n };\n }\n };\n\n // mask element\n svg.Element.mask = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.apply = function (ctx, element) {\n // render as temp svg\n let x = this.attribute('x').toPixels('x');\n let y = this.attribute('y').toPixels('y');\n let width = this.attribute('width').toPixels('x');\n let height = this.attribute('height').toPixels('y');\n\n if (width === 0 && height === 0) {\n const bb = new svg.BoundingBox();\n for (let i = 0; i < this.children.length; i++) {\n bb.addBoundingBox(this.children[i].getBoundingBox());\n }\n x = Math.floor(bb.x1);\n y = Math.floor(bb.y1);\n width = Math.floor(bb.width());\n height = Math.floor(bb.height());\n }\n\n // temporarily remove mask to avoid recursion\n const mask = element.attribute('mask').value;\n element.attribute('mask').value = '';\n\n const cMask = document.createElement('canvas');\n cMask.width = x + width;\n cMask.height = y + height;\n const maskCtx = cMask.getContext('2d');\n this.renderChildren(maskCtx);\n\n const c = document.createElement('canvas');\n c.width = x + width;\n c.height = y + height;\n const tempCtx = c.getContext('2d');\n element.render(tempCtx);\n tempCtx.globalCompositeOperation = 'destination-in';\n tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat');\n tempCtx.fillRect(0, 0, x + width, y + height);\n\n ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat');\n ctx.fillRect(0, 0, x + width, y + height);\n\n // reassign mask\n element.attribute('mask').value = mask;\n };\n\n this.render = function (ctx) {\n // NO RENDER\n };\n }\n };\n\n // clip element\n svg.Element.clipPath = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.apply = function (ctx) {\n for (let i = 0; i < this.children.length; i++) {\n const child = this.children[i];\n if (typeof child.path !== 'undefined') {\n let transform = null;\n if (child.attribute('transform').hasValue()) {\n transform = new svg.Transform(child.attribute('transform').value);\n transform.apply(ctx);\n }\n child.path(ctx);\n ctx.clip();\n if (transform) { transform.unapply(ctx); }\n }\n }\n };\n\n this.render = function (ctx) {\n // NO RENDER\n };\n }\n };\n\n // filters\n svg.Element.filter = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.apply = function (ctx, element) {\n // render as temp svg\n const bb = element.getBoundingBox();\n const x = Math.floor(bb.x1);\n const y = Math.floor(bb.y1);\n const width = Math.floor(bb.width());\n const height = Math.floor(bb.height());\n\n // temporarily remove filter to avoid recursion\n const filter = element.style('filter').value;\n element.style('filter').value = '';\n\n let px = 0, py = 0;\n for (let i = 0; i < this.children.length; i++) {\n const efd = this.children[i].extraFilterDistance || 0;\n px = Math.max(px, efd);\n py = Math.max(py, efd);\n }\n\n const c = document.createElement('canvas');\n c.width = width + 2 * px;\n c.height = height + 2 * py;\n const tempCtx = c.getContext('2d');\n tempCtx.translate(-x + px, -y + py);\n element.render(tempCtx);\n\n // apply filters\n for (let i = 0; i < this.children.length; i++) {\n this.children[i].apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py);\n }\n\n // render on me\n ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py);\n\n // reassign filter\n element.style('filter', true).value = filter;\n };\n\n this.render = function (ctx) {\n // NO RENDER\n };\n }\n };\n\n svg.Element.feMorphology = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.apply = function (ctx, x, y, width, height) {\n // TODO: implement\n };\n }\n };\n\n svg.Element.feComposite = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.apply = function (ctx, x, y, width, height) {\n // TODO: implement\n };\n }\n };\n\n svg.Element.feColorMatrix = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n let matrix = svg.ToNumberArray(this.attribute('values').value);\n switch (this.attribute('type').valueOrDefault('matrix')) { // https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement\n case 'saturate':\n const s = matrix[0];\n matrix = [\n 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0,\n 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0,\n 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0,\n 0, 0, 0, 1, 0,\n 0, 0, 0, 0, 1\n ];\n break;\n case 'hueRotate':\n const a = matrix[0] * Math.PI / 180.0;\n const c = function (m1, m2, m3) { return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; };\n matrix = [\n c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0,\n c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0,\n c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0,\n 0, 0, 0, 1, 0,\n 0, 0, 0, 0, 1\n ];\n break;\n case 'luminanceToAlpha':\n matrix = [\n 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0,\n 0, 0, 0, 0, 0,\n 0.2125, 0.7154, 0.0721, 0, 0,\n 0, 0, 0, 0, 1\n ];\n break;\n }\n\n function imGet (img, x, y, width, height, rgba) {\n return img[y * width * 4 + x * 4 + rgba];\n }\n\n function imSet (img, x, y, width, height, rgba, val) {\n img[y * width * 4 + x * 4 + rgba] = val;\n }\n\n function m (i, v) {\n const mi = matrix[i];\n return mi * (mi < 0 ? v - 255 : v);\n }\n\n this.apply = function (ctx, x, y, width, height) {\n // assuming x==0 && y==0 for now\n const srcData = ctx.getImageData(0, 0, width, height);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const r = imGet(srcData.data, x, y, width, height, 0);\n const g = imGet(srcData.data, x, y, width, height, 1);\n const b = imGet(srcData.data, x, y, width, height, 2);\n const a = imGet(srcData.data, x, y, width, height, 3);\n imSet(srcData.data, x, y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1));\n imSet(srcData.data, x, y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1));\n imSet(srcData.data, x, y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1));\n imSet(srcData.data, x, y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1));\n }\n }\n ctx.clearRect(0, 0, width, height);\n ctx.putImageData(srcData, 0, 0);\n };\n }\n };\n\n svg.Element.feGaussianBlur = class extends svg.Element.ElementBase {\n constructor (node) {\n super(node);\n\n this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue());\n this.extraFilterDistance = this.blurRadius;\n\n this.apply = function (ctx, x, y, width, height) {\n if (typeof stackBlurCanvasRGBA === 'undefined') {\n svg.log('ERROR: StackBlur.js must be included for blur to work');\n return;\n }\n\n // StackBlur requires canvas be on document\n ctx.canvas.id = svg.UniqueId();\n ctx.canvas.style.display = 'none';\n document.body.appendChild(ctx.canvas);\n stackBlurCanvasRGBA(ctx.canvas.id, x, y, width, height, this.blurRadius);\n document.body.removeChild(ctx.canvas);\n };\n }\n };\n\n // title element, do nothing\n svg.Element.title = class extends svg.Element.ElementBase {\n constructor (node) {\n super();\n }\n };\n\n // desc element, do nothing\n svg.Element.desc = class extends svg.Element.ElementBase {\n constructor (node) {\n super();\n }\n };\n\n svg.Element.MISSING = class extends svg.Element.ElementBase {\n constructor (node) {\n super();\n svg.log('ERROR: Element \\'' + node.nodeName + '\\' not yet implemented.');\n }\n };\n\n // element factory\n svg.CreateElement = function (node) {\n const className = node.nodeName\n .replace(/^[^:]+:/, '') // remove namespace\n .replace(/-/g, ''); // remove dashes\n let e;\n if (typeof svg.Element[className] !== 'undefined') {\n e = new svg.Element[className](node);\n } else {\n e = new svg.Element.MISSING(node);\n }\n\n e.type = node.nodeName;\n return e;\n };\n\n // load from url\n svg.load = function (ctx, url) {\n svg.loadXml(ctx, svg.ajax(url));\n };\n\n // load from xml\n svg.loadXml = function (ctx, xml) {\n svg.loadXmlDoc(ctx, svg.parseXml(xml));\n };\n\n svg.loadXmlDoc = function (ctx, dom) {\n svg.init(ctx);\n\n const mapXY = function (p) {\n let e = ctx.canvas;\n while (e) {\n p.x -= e.offsetLeft;\n p.y -= e.offsetTop;\n e = e.offsetParent;\n }\n if (window.scrollX) p.x += window.scrollX;\n if (window.scrollY) p.y += window.scrollY;\n return p;\n };\n\n // bind mouse\n if (svg.opts['ignoreMouse'] !== true) {\n ctx.canvas.onclick = function (e) {\n const p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY));\n svg.Mouse.onclick(p.x, p.y);\n };\n ctx.canvas.onmousemove = function (e) {\n const p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY));\n svg.Mouse.onmousemove(p.x, p.y);\n };\n }\n\n const e = svg.CreateElement(dom.documentElement);\n e.root = true;\n\n // render loop\n let isFirstRender = true;\n const draw = function () {\n svg.ViewPort.Clear();\n if (ctx.canvas.parentNode) svg.ViewPort.SetCurrent(ctx.canvas.parentNode.clientWidth, ctx.canvas.parentNode.clientHeight);\n\n if (svg.opts['ignoreDimensions'] !== true) {\n // set canvas size\n if (e.style('width').hasValue()) {\n ctx.canvas.width = e.style('width').toPixels('x');\n ctx.canvas.style.width = ctx.canvas.width + 'px';\n }\n if (e.style('height').hasValue()) {\n ctx.canvas.height = e.style('height').toPixels('y');\n ctx.canvas.style.height = ctx.canvas.height + 'px';\n }\n }\n let cWidth = ctx.canvas.clientWidth || ctx.canvas.width;\n let cHeight = ctx.canvas.clientHeight || ctx.canvas.height;\n if (svg.opts['ignoreDimensions'] === true && e.style('width').hasValue() && e.style('height').hasValue()) {\n cWidth = e.style('width').toPixels('x');\n cHeight = e.style('height').toPixels('y');\n }\n svg.ViewPort.SetCurrent(cWidth, cHeight);\n\n if (svg.opts['offsetX'] != null) e.attribute('x', true).value = svg.opts['offsetX'];\n if (svg.opts['offsetY'] != null) e.attribute('y', true).value = svg.opts['offsetY'];\n if (svg.opts['scaleWidth'] != null || svg.opts['scaleHeight'] != null) {\n const viewBox = svg.ToNumberArray(e.attribute('viewBox').value);\n let xRatio = null, yRatio = null;\n\n if (svg.opts['scaleWidth'] != null) {\n if (e.attribute('width').hasValue()) xRatio = e.attribute('width').toPixels('x') / svg.opts['scaleWidth'];\n else if (!isNaN(viewBox[2])) xRatio = viewBox[2] / svg.opts['scaleWidth'];\n }\n\n if (svg.opts['scaleHeight'] != null) {\n if (e.attribute('height').hasValue()) yRatio = e.attribute('height').toPixels('y') / svg.opts['scaleHeight'];\n else if (!isNaN(viewBox[3])) yRatio = viewBox[3] / svg.opts['scaleHeight'];\n }\n\n if (xRatio == null) { xRatio = yRatio; }\n if (yRatio == null) { yRatio = xRatio; }\n\n e.attribute('width', true).value = svg.opts['scaleWidth'];\n e.attribute('height', true).value = svg.opts['scaleHeight'];\n e.attribute('viewBox', true).value = '0 0 ' + (cWidth * xRatio) + ' ' + (cHeight * yRatio);\n e.attribute('preserveAspectRatio', true).value = 'none';\n }\n\n // clear and render\n if (svg.opts['ignoreClear'] !== true) {\n ctx.clearRect(0, 0, cWidth, cHeight);\n }\n e.render(ctx);\n if (isFirstRender) {\n isFirstRender = false;\n if (typeof svg.opts['renderCallback'] === 'function') {\n svg.opts['renderCallback'](dom);\n }\n }\n };\n\n let waitingForImages = true;\n if (svg.ImagesLoaded()) {\n waitingForImages = false;\n draw();\n }\n svg.intervalID = setInterval(function () {\n let needUpdate = false;\n\n if (waitingForImages && svg.ImagesLoaded()) {\n waitingForImages = false;\n needUpdate = true;\n }\n\n // need update from mouse events?\n if (svg.opts['ignoreMouse'] !== true) {\n needUpdate = needUpdate | svg.Mouse.hasEvents();\n }\n\n // need update from animations?\n if (svg.opts['ignoreAnimation'] !== true) {\n for (let i = 0; i < svg.Animations.length; i++) {\n needUpdate = needUpdate | svg.Animations[i].update(1000 / svg.FRAMERATE);\n }\n }\n\n // need update from redraw?\n if (typeof svg.opts['forceRedraw'] === 'function') {\n if (svg.opts['forceRedraw']() === true) needUpdate = true;\n }\n\n // render if needed\n if (needUpdate) {\n draw();\n svg.Mouse.runEvents(); // run and clear our events\n }\n }, 1000 / svg.FRAMERATE);\n };\n\n svg.stop = function () {\n if (svg.intervalID) {\n clearInterval(svg.intervalID);\n }\n };\n\n svg.Mouse = new function () {\n this.events = [];\n this.hasEvents = function () { return this.events.length !== 0; };\n\n this.onclick = function (x, y) {\n this.events.push({ type: 'onclick', x, y,\n run (e) { if (e.onclick) e.onclick(); }\n });\n };\n\n this.onmousemove = function (x, y) {\n this.events.push({ type: 'onmousemove', x, y,\n run (e) { if (e.onmousemove) e.onmousemove(); }\n });\n };\n\n this.eventElements = [];\n\n this.checkPath = function (element, ctx) {\n for (let i = 0; i < this.events.length; i++) {\n const e = this.events[i];\n if (ctx.isPointInPath && ctx.isPointInPath(e.x, e.y)) this.eventElements[i] = element;\n }\n };\n\n this.checkBoundingBox = function (element, bb) {\n for (let i = 0; i < this.events.length; i++) {\n const e = this.events[i];\n if (bb.isPointInBox(e.x, e.y)) this.eventElements[i] = element;\n }\n };\n\n this.runEvents = function () {\n svg.ctx.canvas.style.cursor = '';\n\n for (let i = 0; i < this.events.length; i++) {\n const e = this.events[i];\n let element = this.eventElements[i];\n while (element) {\n e.run(element);\n element = element.parent;\n }\n }\n\n // done running, clear\n this.events = [];\n this.eventElements = [];\n };\n }();\n\n return svg;\n}\n\nif (typeof CanvasRenderingContext2D !== 'undefined') {\n CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh) {\n canvg(this.canvas, s, {\n ignoreMouse: true,\n ignoreAnimation: true,\n ignoreDimensions: true,\n ignoreClear: true,\n offsetX: dx,\n offsetY: dy,\n scaleWidth: dw,\n scaleHeight: dh\n });\n };\n}\n","/* globals jQuery */\n/**\n * Licensed under the MIT License\n *\n * Copyright(c) 2011 Jeff Schiller\n * Copyright(c) 2016 Flint O'Brien\n */\n\nimport {NS} from './svgedit.js';\nimport {toXml, walkTree} from './svgutils.js';\n\nconst $ = jQuery;\n\n/**\n * This class encapsulates the concept of a layer in the drawing. It can be constructed with\n * an existing group element or, with three parameters, will create a new layer group element.\n *\n * Usage:\n * new Layer'name', group) // Use the existing group for this layer.\n * new Layer('name', group, svgElem) // Create a new group and add it to the DOM after group.\n * new Layer('name', null, svgElem) // Create a new group and add it to the DOM as the last layer.\n *\n * @param {string} name - Layer name\n * @param {SVGGElement|null} group - An existing SVG group element or null.\n * If group and no svgElem, use group for this layer.\n * If group and svgElem, create a new group element and insert it in the DOM after group.\n * If no group and svgElem, create a new group element and insert it in the DOM as the last layer.\n * @param {SVGGElement=} svgElem - The SVG DOM element. If defined, use this to add\n * a new layer to the document.\n */\nexport default class Layer {\n constructor (name, group, svgElem) {\n this.name_ = name;\n this.group_ = svgElem ? null : group;\n\n if (svgElem) {\n // Create a group element with title and add it to the DOM.\n const svgdoc = svgElem.ownerDocument;\n this.group_ = svgdoc.createElementNS(NS.SVG, 'g');\n const layerTitle = svgdoc.createElementNS(NS.SVG, 'title');\n layerTitle.textContent = name;\n this.group_.appendChild(layerTitle);\n if (group) {\n $(group).after(this.group_);\n } else {\n svgElem.appendChild(this.group_);\n }\n }\n\n addLayerClass(this.group_);\n walkTree(this.group_, function (e) {\n e.setAttribute('style', 'pointer-events:inherit');\n });\n\n this.group_.setAttribute('style', svgElem ? 'pointer-events:all' : 'pointer-events:none');\n }\n\n /**\n * Get the layer's name.\n * @returns {string} The layer name\n */\n getName () {\n return this.name_;\n }\n\n /**\n * Get the group element for this layer.\n * @returns {SVGGElement} The layer SVG group\n */\n getGroup () {\n return this.group_;\n }\n\n /**\n * Active this layer so it takes pointer events.\n */\n activate () {\n this.group_.setAttribute('style', 'pointer-events:all');\n }\n\n /**\n * Deactive this layer so it does NOT take pointer events.\n */\n deactivate () {\n this.group_.setAttribute('style', 'pointer-events:none');\n }\n\n /**\n * Set this layer visible or hidden based on 'visible' parameter.\n * @param {boolean} visible - If true, make visible; otherwise, hide it.\n */\n setVisible (visible) {\n const expected = visible === undefined || visible ? 'inline' : 'none';\n const oldDisplay = this.group_.getAttribute('display');\n if (oldDisplay !== expected) {\n this.group_.setAttribute('display', expected);\n }\n }\n\n /**\n * Is this layer visible?\n * @returns {boolean} True if visible.\n */\n isVisible () {\n return this.group_.getAttribute('display') !== 'none';\n }\n\n /**\n * Get layer opacity.\n * @returns {number} Opacity value.\n */\n getOpacity () {\n const opacity = this.group_.getAttribute('opacity');\n if (opacity === null || opacity === undefined) {\n return 1;\n }\n return parseFloat(opacity);\n }\n\n /**\n * Sets the opacity of this layer. If opacity is not a value between 0.0 and 1.0,\n * nothing happens.\n * @param {number} opacity - A float value in the range 0.0-1.0\n */\n setOpacity (opacity) {\n if (typeof opacity === 'number' && opacity >= 0.0 && opacity <= 1.0) {\n this.group_.setAttribute('opacity', opacity);\n }\n }\n\n /**\n * Append children to this layer.\n * @param {SVGGElement} children - The children to append to this layer.\n */\n appendChildren (children) {\n for (let i = 0; i < children.length; ++i) {\n this.group_.appendChild(children[i]);\n }\n }\n\n getTitleElement () {\n const len = this.group_.childNodes.length;\n for (let i = 0; i < len; ++i) {\n const child = this.group_.childNodes.item(i);\n if (child && child.tagName === 'title') {\n return child;\n }\n }\n return null;\n }\n\n /**\n * Set the name of this layer.\n * @param {string} name - The new name.\n * @param {svgedit.history.HistoryRecordingService} hrService - History recording service\n * @returns {string|null} The new name if changed; otherwise, null.\n */\n setName (name, hrService) {\n const previousName = this.name_;\n name = toXml(name);\n // now change the underlying title element contents\n const title = this.getTitleElement();\n if (title) {\n $(title).empty();\n title.textContent = name;\n this.name_ = name;\n if (hrService) {\n hrService.changeElement(title, {'#text': previousName});\n }\n return this.name_;\n }\n return null;\n }\n\n /**\n * Remove this layer's group from the DOM. No more functions on group can be called after this.\n * @param {SVGGElement} children - The children to append to this layer.\n * @returns {SVGGElement} The layer SVG group that was just removed.\n */\n removeGroup () {\n const parent = this.group_.parentNode;\n const group = parent.removeChild(this.group_);\n this.group_ = undefined;\n return group;\n }\n}\n/**\n * @property {string} CLASS_NAME - class attribute assigned to all layer groups.\n */\nLayer.CLASS_NAME = 'layer';\n\n/**\n * @property {RegExp} CLASS_REGEX - Used to test presence of class Layer.CLASS_NAME\n */\nLayer.CLASS_REGEX = new RegExp('(\\\\s|^)' + Layer.CLASS_NAME + '(\\\\s|$)');\n\n/**\n * Add class Layer.CLASS_NAME to the element (usually class='layer').\n *\n * Parameters:\n * @param {SVGGElement} elem - The SVG element to update\n */\nfunction addLayerClass (elem) {\n const classes = elem.getAttribute('class');\n if (classes === null || classes === undefined || !classes.length) {\n elem.setAttribute('class', Layer.CLASS_NAME);\n } else if (!Layer.CLASS_REGEX.test(classes)) {\n elem.setAttribute('class', classes + ' ' + Layer.CLASS_NAME);\n }\n}\n","/**\n * Package: svgedit.history\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2016 Flint O'Brien\n */\n\nimport {\n BatchCommand, MoveElementCommand, InsertElementCommand, RemoveElementCommand,\n ChangeElementCommand\n} from './history.js';\n\n/**\n * History recording service.\n *\n * A self-contained service interface for recording history. Once injected, no other dependencies\n * or globals are required (example: UndoManager, command types, etc.). Easy to mock for unit tests.\n * Built on top of history classes in history.js.\n *\n * There is a simple start/end interface for batch commands.\n *\n * HistoryRecordingService.NO_HISTORY is a singleton that can be passed in to functions\n * that record history. This helps when the caller requires that no history be recorded.\n *\n * Usage:\n * The following will record history: insert, batch, insert.\n * ```\n * hrService = new svgedit.history.HistoryRecordingService(this.undoMgr);\n * hrService.insertElement(elem, text); // add simple command to history.\n * hrService.startBatchCommand('create two elements');\n * hrService.changeElement(elem, attrs, text); // add to batchCommand\n * hrService.changeElement(elem, attrs2, text); // add to batchCommand\n * hrService.endBatchCommand(); // add batch command with two change commands to history.\n * hrService.insertElement(elem, text); // add simple command to history.\n * ```\n *\n * Note that all functions return this, so commands can be chained, like so:\n *\n * ```\n * hrService\n * .startBatchCommand('create two elements')\n * .insertElement(elem, text)\n * .changeElement(elem, attrs, text)\n * .endBatchCommand();\n * ```\n *\n * @param {svgedit.history.UndoManager} undoManager - The undo manager.\n * A value of null is valid for cases where no history recording is required.\n * See singleton: HistoryRecordingService.NO_HISTORY\n */\nexport default class HistoryRecordingService {\n constructor (undoManager) {\n this.undoManager_ = undoManager;\n this.currentBatchCommand_ = null;\n this.batchCommandStack_ = [];\n }\n\n /**\n * Start a batch command so multiple commands can recorded as a single history command.\n * Requires a corresponding call to endBatchCommand. Start and end commands can be nested.\n *\n * @param {string} text - Optional string describing the batch command.\n * @returns {svgedit.history.HistoryRecordingService}\n */\n startBatchCommand (text) {\n if (!this.undoManager_) { return this; }\n this.currentBatchCommand_ = new BatchCommand(text);\n this.batchCommandStack_.push(this.currentBatchCommand_);\n return this;\n }\n\n /**\n * End a batch command and add it to the history or a parent batch command.\n * @returns {svgedit.history.HistoryRecordingService}\n */\n endBatchCommand () {\n if (!this.undoManager_) { return this; }\n if (this.currentBatchCommand_) {\n const batchCommand = this.currentBatchCommand_;\n this.batchCommandStack_.pop();\n const {length} = this.batchCommandStack_;\n this.currentBatchCommand_ = length ? this.batchCommandStack_[length - 1] : null;\n this.addCommand_(batchCommand);\n }\n return this;\n }\n\n /**\n * Add a MoveElementCommand to the history or current batch command\n * @param {Element} elem - The DOM element that was moved\n * @param {Element} oldNextSibling - The element's next sibling before it was moved\n * @param {Element} oldParent - The element's parent before it was moved\n * @param {string} [text] - An optional string visible to user related to this change\n * @returns {svgedit.history.HistoryRecordingService}\n */\n moveElement (elem, oldNextSibling, oldParent, text) {\n if (!this.undoManager_) { return this; }\n this.addCommand_(new MoveElementCommand(elem, oldNextSibling, oldParent, text));\n return this;\n }\n\n /**\n * Add an InsertElementCommand to the history or current batch command\n * @param {Element} elem - The DOM element that was added\n * @param {string} [text] - An optional string visible to user related to this change\n * @returns {svgedit.history.HistoryRecordingService}\n */\n insertElement (elem, text) {\n if (!this.undoManager_) { return this; }\n this.addCommand_(new InsertElementCommand(elem, text));\n return this;\n }\n\n /**\n * Add a RemoveElementCommand to the history or current batch command\n * @param {Element} elem - The DOM element that was removed\n * @param {Element} oldNextSibling - The element's next sibling before it was removed\n * @param {Element} oldParent - The element's parent before it was removed\n * @param {string} [text] - An optional string visible to user related to this change\n * @returns {svgedit.history.HistoryRecordingService}\n */\n removeElement (elem, oldNextSibling, oldParent, text) {\n if (!this.undoManager_) { return this; }\n this.addCommand_(new RemoveElementCommand(elem, oldNextSibling, oldParent, text));\n return this;\n }\n\n /**\n * Add a ChangeElementCommand to the history or current batch command\n * @param {Element} elem - The DOM element that was changed\n * @param {Object} attrs - An object with the attributes to be changed and the values they had *before* the change\n * @param {string} [text] - An optional string visible to user related to this change\n * @returns {svgedit.history.HistoryRecordingService}\n */\n changeElement (elem, attrs, text) {\n if (!this.undoManager_) { return this; }\n this.addCommand_(new ChangeElementCommand(elem, attrs, text));\n return this;\n }\n\n /**\n * Private function to add a command to the history or current batch command.\n * @param cmd\n * @returns {svgedit.history.HistoryRecordingService}\n * @private\n */\n addCommand_ (cmd) {\n if (!this.undoManager_) { return this; }\n if (this.currentBatchCommand_) {\n this.currentBatchCommand_.addSubCommand(cmd);\n } else {\n this.undoManager_.addCommandToHistory(cmd);\n }\n }\n}\n/**\n * @property {HistoryRecordingService} NO_HISTORY - Singleton that can be passed to functions that record history, but the caller requires that no history be recorded.\n */\nHistoryRecordingService.NO_HISTORY = new HistoryRecordingService();\n","/* globals jQuery */\n/**\n * Package: svgedit.draw\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2011 Jeff Schiller\n */\n\nimport Layer from './layer.js';\nimport HistoryRecordingService from './historyrecording.js';\n\nimport {NS} from './svgedit.js';\nimport {isOpera} from './browser.js';\nimport {\n toXml, getElem,\n copyElem as utilCopyElem\n} from './svgutils.js';\nimport {\n BatchCommand, RemoveElementCommand, MoveElementCommand, ChangeElementCommand\n} from './history.js';\n\nconst $ = jQuery;\n\nconst visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(',');\n\nconst RandomizeModes = {\n LET_DOCUMENT_DECIDE: 0,\n ALWAYS_RANDOMIZE: 1,\n NEVER_RANDOMIZE: 2\n};\nlet randIds = RandomizeModes.LET_DOCUMENT_DECIDE;\n// Array with current disabled elements (for in-group editing)\nlet disabledElems = [];\n\n/**\n * Get a HistoryRecordingService.\n * @param {svgedit.history.HistoryRecordingService=} hrService - if exists, return it instead of creating a new service.\n * @returns {svgedit.history.HistoryRecordingService}\n */\nfunction historyRecordingService (hrService) {\n return hrService || new HistoryRecordingService(canvas_.undoMgr);\n}\n\n/**\n * Find the layer name in a group element.\n * @param group The group element to search in.\n * @returns {string} The layer name or empty string.\n */\nfunction findLayerNameInGroup (group) {\n return $('title', group).text() ||\n (isOpera() && group.querySelectorAll\n // Hack for Opera 10.60\n ? $(group.querySelectorAll('title')).text()\n : '');\n}\n\n/**\n * Given a set of names, return a new unique name.\n * @param {Array.} existingLayerNames - Existing layer names.\n * @returns {string} - The new name.\n */\nfunction getNewLayerName (existingLayerNames) {\n let i = 1;\n // TODO(codedread): What about internationalization of \"Layer\"?\n while (existingLayerNames.includes(('Layer ' + i))) { i++; }\n return 'Layer ' + i;\n}\n\n/**\n * This class encapsulates the concept of a SVG-edit drawing\n * @param {SVGSVGElement} svgElem - The SVG DOM Element that this JS object\n * encapsulates. If the svgElem has a se:nonce attribute on it, then\n * IDs will use the nonce as they are generated.\n * @param {String} [optIdPrefix=svg_] - The ID prefix to use.\n */\nexport class Drawing {\n constructor (svgElem, optIdPrefix) {\n if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI ||\n svgElem.tagName !== 'svg' || svgElem.namespaceURI !== NS.SVG) {\n throw new Error('Error: svgedit.draw.Drawing instance initialized without a element');\n }\n\n /**\n * The SVG DOM Element that represents this drawing.\n * @type {SVGSVGElement}\n */\n this.svgElem_ = svgElem;\n\n /**\n * The latest object number used in this drawing.\n * @type {number}\n */\n this.obj_num = 0;\n\n /**\n * The prefix to prepend to each element id in the drawing.\n * @type {String}\n */\n this.idPrefix = optIdPrefix || 'svg_';\n\n /**\n * An array of released element ids to immediately reuse.\n * @type {Array.}\n */\n this.releasedNums = [];\n\n /**\n * The z-ordered array of Layer objects. Each layer has a name\n * and group element.\n * The first layer is the one at the bottom of the rendering.\n * @type {Array.}\n */\n this.all_layers = [];\n\n /**\n * Map of all_layers by name.\n *\n * Note: Layers are ordered, but referenced externally by name; so, we need both container\n * types depending on which function is called (i.e. all_layers and layer_map).\n *\n * @type {Object.}\n */\n this.layer_map = {};\n\n /**\n * The current layer being used.\n * @type {Layer}\n */\n this.current_layer = null;\n\n /**\n * The nonce to use to uniquely identify elements across drawings.\n * @type {!String}\n */\n this.nonce_ = '';\n const n = this.svgElem_.getAttributeNS(NS.SE, 'nonce');\n // If already set in the DOM, use the nonce throughout the document\n // else, if randomizeIds(true) has been called, create and set the nonce.\n if (!!n && randIds !== RandomizeModes.NEVER_RANDOMIZE) {\n this.nonce_ = n;\n } else if (randIds === RandomizeModes.ALWAYS_RANDOMIZE) {\n this.setNonce(Math.floor(Math.random() * 100001));\n }\n }\n\n /**\n * @param {string} id Element ID to retrieve\n * @returns {Element} SVG element within the root SVGSVGElement\n */\n getElem_ (id) {\n if (this.svgElem_.querySelector) {\n // querySelector lookup\n return this.svgElem_.querySelector('#' + id);\n }\n // jQuery lookup: twice as slow as xpath in FF\n return $(this.svgElem_).find('[id=' + id + ']')[0];\n }\n\n /**\n * @returns {SVGSVGElement}\n */\n getSvgElem () {\n return this.svgElem_;\n }\n\n /**\n * @returns {!string|number} The previously set nonce\n */\n getNonce () {\n return this.nonce_;\n }\n\n /**\n * @param {!string|number} n The nonce to set\n */\n setNonce (n) {\n this.svgElem_.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE);\n this.svgElem_.setAttributeNS(NS.SE, 'se:nonce', n);\n this.nonce_ = n;\n }\n\n /**\n * Clears any previously set nonce\n */\n clearNonce () {\n // We deliberately leave any se:nonce attributes alone,\n // we just don't use it to randomize ids.\n this.nonce_ = '';\n }\n\n /**\n * Returns the latest object id as a string.\n * @return {String} The latest object Id.\n */\n getId () {\n return this.nonce_\n ? this.idPrefix + this.nonce_ + '_' + this.obj_num\n : this.idPrefix + this.obj_num;\n }\n\n /**\n * Returns the next object Id as a string.\n * @return {String} The next object Id to use.\n */\n getNextId () {\n const oldObjNum = this.obj_num;\n let restoreOldObjNum = false;\n\n // If there are any released numbers in the release stack,\n // use the last one instead of the next obj_num.\n // We need to temporarily use obj_num as that is what getId() depends on.\n if (this.releasedNums.length > 0) {\n this.obj_num = this.releasedNums.pop();\n restoreOldObjNum = true;\n } else {\n // If we are not using a released id, then increment the obj_num.\n this.obj_num++;\n }\n\n // Ensure the ID does not exist.\n let id = this.getId();\n while (this.getElem_(id)) {\n if (restoreOldObjNum) {\n this.obj_num = oldObjNum;\n restoreOldObjNum = false;\n }\n this.obj_num++;\n id = this.getId();\n }\n // Restore the old object number if required.\n if (restoreOldObjNum) {\n this.obj_num = oldObjNum;\n }\n return id;\n }\n\n /**\n * Releases the object Id, letting it be used as the next id in getNextId().\n * This method DOES NOT remove any elements from the DOM, it is expected\n * that client code will do this.\n * @param {string} id - The id to release.\n * @returns {boolean} True if the id was valid to be released, false otherwise.\n */\n releaseId (id) {\n // confirm if this is a valid id for this Document, else return false\n const front = this.idPrefix + (this.nonce_ ? this.nonce_ + '_' : '');\n if (typeof id !== 'string' || !id.startsWith(front)) {\n return false;\n }\n // extract the obj_num of this id\n const num = parseInt(id.substr(front.length), 10);\n\n // if we didn't get a positive number or we already released this number\n // then return false.\n if (typeof num !== 'number' || num <= 0 || this.releasedNums.includes(num)) {\n return false;\n }\n\n // push the released number into the released queue\n this.releasedNums.push(num);\n\n return true;\n }\n\n /**\n * Returns the number of layers in the current drawing.\n * @returns {integer} The number of layers in the current drawing.\n */\n getNumLayers () {\n return this.all_layers.length;\n }\n\n /**\n * Check if layer with given name already exists\n * @param {string} name - The layer name to check\n */\n hasLayer (name) {\n return this.layer_map[name] !== undefined;\n }\n\n /**\n * Returns the name of the ith layer. If the index is out of range, an empty string is returned.\n * @param {integer} i - The zero-based index of the layer you are querying.\n * @returns {string} The name of the ith layer (or the empty string if none found)\n */\n getLayerName (i) {\n return i >= 0 && i < this.getNumLayers() ? this.all_layers[i].getName() : '';\n }\n\n /**\n * @returns {SVGGElement} The SVGGElement representing the current layer.\n */\n getCurrentLayer () {\n return this.current_layer ? this.current_layer.getGroup() : null;\n }\n\n /**\n * Get a layer by name.\n * @returns {SVGGElement} The SVGGElement representing the named layer or null.\n */\n getLayerByName (name) {\n const layer = this.layer_map[name];\n return layer ? layer.getGroup() : null;\n }\n\n /**\n * Returns the name of the currently selected layer. If an error occurs, an empty string\n * is returned.\n * @returns {string} The name of the currently active layer (or the empty string if none found).\n */\n getCurrentLayerName () {\n return this.current_layer ? this.current_layer.getName() : '';\n }\n\n /**\n * Set the current layer's name.\n * @param {string} name - The new name.\n * @param {svgedit.history.HistoryRecordingService} hrService - History recording service\n * @returns {string|null} The new name if changed; otherwise, null.\n */\n setCurrentLayerName (name, hrService) {\n let finalName = null;\n if (this.current_layer) {\n const oldName = this.current_layer.getName();\n finalName = this.current_layer.setName(name, hrService);\n if (finalName) {\n delete this.layer_map[oldName];\n this.layer_map[finalName] = this.current_layer;\n }\n }\n return finalName;\n }\n\n /**\n * Set the current layer's position.\n * @param {number} newpos - The zero-based index of the new position of the layer. Range should be 0 to layers-1\n * @returns {Object} If the name was changed, returns {title:SVGGElement, previousName:string}; otherwise null.\n */\n setCurrentLayerPosition (newpos) {\n const layerCount = this.getNumLayers();\n if (!this.current_layer || newpos < 0 || newpos >= layerCount) {\n return null;\n }\n\n let oldpos;\n for (oldpos = 0; oldpos < layerCount; ++oldpos) {\n if (this.all_layers[oldpos] === this.current_layer) { break; }\n }\n // some unknown error condition (current_layer not in all_layers)\n if (oldpos === layerCount) { return null; }\n\n if (oldpos !== newpos) {\n // if our new position is below us, we need to insert before the node after newpos\n const currentGroup = this.current_layer.getGroup();\n const oldNextSibling = currentGroup.nextSibling;\n\n let refGroup = null;\n if (newpos > oldpos) {\n if (newpos < layerCount - 1) {\n refGroup = this.all_layers[newpos + 1].getGroup();\n }\n // if our new position is above us, we need to insert before the node at newpos\n } else {\n refGroup = this.all_layers[newpos].getGroup();\n }\n this.svgElem_.insertBefore(currentGroup, refGroup);\n\n this.identifyLayers();\n this.setCurrentLayer(this.getLayerName(newpos));\n\n return {\n currentGroup,\n oldNextSibling\n };\n }\n return null;\n }\n\n mergeLayer (hrService) {\n const currentGroup = this.current_layer.getGroup();\n const prevGroup = $(currentGroup).prev()[0];\n if (!prevGroup) { return; }\n\n hrService.startBatchCommand('Merge Layer');\n\n const layerNextSibling = currentGroup.nextSibling;\n hrService.removeElement(currentGroup, layerNextSibling, this.svgElem_);\n\n while (currentGroup.firstChild) {\n const child = currentGroup.firstChild;\n if (child.localName === 'title') {\n hrService.removeElement(child, child.nextSibling, currentGroup);\n currentGroup.removeChild(child);\n continue;\n }\n const oldNextSibling = child.nextSibling;\n prevGroup.appendChild(child);\n hrService.moveElement(child, oldNextSibling, currentGroup);\n }\n\n // Remove current layer's group\n this.current_layer.removeGroup();\n // Remove the current layer and set the previous layer as the new current layer\n const index = this.all_layers.indexOf(this.current_layer);\n if (index > 0) {\n const name = this.current_layer.getName();\n this.current_layer = this.all_layers[index - 1];\n this.all_layers.splice(index, 1);\n delete this.layer_map[name];\n }\n\n hrService.endBatchCommand();\n }\n\n mergeAllLayers (hrService) {\n // Set the current layer to the last layer.\n this.current_layer = this.all_layers[this.all_layers.length - 1];\n\n hrService.startBatchCommand('Merge all Layers');\n while (this.all_layers.length > 1) {\n this.mergeLayer(hrService);\n }\n hrService.endBatchCommand();\n }\n\n /**\n * Sets the current layer. If the name is not a valid layer name, then this\n * function returns false. Otherwise it returns true. This is not an\n * undo-able action.\n * @param {string} name - The name of the layer you want to switch to.\n * @returns {boolean} true if the current layer was switched, otherwise false\n */\n setCurrentLayer (name) {\n const layer = this.layer_map[name];\n if (layer) {\n if (this.current_layer) {\n this.current_layer.deactivate();\n }\n this.current_layer = layer;\n this.current_layer.activate();\n return true;\n }\n return false;\n }\n\n /**\n * Deletes the current layer from the drawing and then clears the selection.\n * This function then calls the 'changed' handler. This is an undoable action.\n * @returns {SVGGElement} The SVGGElement of the layer removed or null.\n */\n deleteCurrentLayer () {\n if (this.current_layer && this.getNumLayers() > 1) {\n const oldLayerGroup = this.current_layer.removeGroup();\n this.identifyLayers();\n return oldLayerGroup;\n }\n return null;\n }\n\n /**\n * Updates layer system and sets the current layer to the\n * top-most layer (last child of this drawing).\n */\n identifyLayers () {\n this.all_layers = [];\n this.layer_map = {};\n const numchildren = this.svgElem_.childNodes.length;\n // loop through all children of SVG element\n const orphans = [], layernames = [];\n let layer = null;\n let childgroups = false;\n for (let i = 0; i < numchildren; ++i) {\n const child = this.svgElem_.childNodes.item(i);\n // for each g, find its layer name\n if (child && child.nodeType === 1) {\n if (child.tagName === 'g') {\n childgroups = true;\n const name = findLayerNameInGroup(child);\n if (name) {\n layernames.push(name);\n layer = new Layer(name, child);\n this.all_layers.push(layer);\n this.layer_map[name] = layer;\n } else {\n // if group did not have a name, it is an orphan\n orphans.push(child);\n }\n } else if (visElems.includes(child.nodeName)) {\n // Child is \"visible\" (i.e. not a or element), so it is an orphan\n orphans.push(child);\n }\n }\n }\n\n // If orphans or no layers found, create a new layer and add all the orphans to it\n if (orphans.length > 0 || !childgroups) {\n layer = new Layer(getNewLayerName(layernames), null, this.svgElem_);\n layer.appendChildren(orphans);\n this.all_layers.push(layer);\n this.layer_map[name] = layer;\n } else {\n layer.activate();\n }\n this.current_layer = layer;\n }\n\n /**\n * Creates a new top-level layer in the drawing with the given name and\n * makes it the current layer.\n * @param {string} name - The given name. If the layer name exists, a new name will be generated.\n * @param {svgedit.history.HistoryRecordingService} hrService - History recording service\n * @returns {SVGGElement} The SVGGElement of the new layer, which is\n * also the current layer of this drawing.\n */\n createLayer (name, hrService) {\n if (this.current_layer) {\n this.current_layer.deactivate();\n }\n // Check for duplicate name.\n if (name === undefined || name === null || name === '' || this.layer_map[name]) {\n name = getNewLayerName(Object.keys(this.layer_map));\n }\n\n // Crate new layer and add to DOM as last layer\n const layer = new Layer(name, null, this.svgElem_);\n // Like to assume hrService exists, but this is backwards compatible with old version of createLayer.\n if (hrService) {\n hrService.startBatchCommand('Create Layer');\n hrService.insertElement(layer.getGroup());\n hrService.endBatchCommand();\n }\n\n this.all_layers.push(layer);\n this.layer_map[name] = layer;\n this.current_layer = layer;\n return layer.getGroup();\n }\n\n /**\n * Creates a copy of the current layer with the given name and makes it the current layer.\n * @param {string} name - The given name. If the layer name exists, a new name will be generated.\n * @param {svgedit.history.HistoryRecordingService} hrService - History recording service\n * @returns {SVGGElement} The SVGGElement of the new layer, which is\n * also the current layer of this drawing.\n */\n cloneLayer (name, hrService) {\n if (!this.current_layer) { return null; }\n this.current_layer.deactivate();\n // Check for duplicate name.\n if (name === undefined || name === null || name === '' || this.layer_map[name]) {\n name = getNewLayerName(Object.keys(this.layer_map));\n }\n\n // Create new group and add to DOM just after current_layer\n const currentGroup = this.current_layer.getGroup();\n const layer = new Layer(name, currentGroup, this.svgElem_);\n const group = layer.getGroup();\n\n // Clone children\n const children = currentGroup.childNodes;\n for (let index = 0; index < children.length; index++) {\n const ch = children[index];\n if (ch.localName === 'title') { continue; }\n group.appendChild(this.copyElem(ch));\n }\n\n if (hrService) {\n hrService.startBatchCommand('Duplicate Layer');\n hrService.insertElement(group);\n hrService.endBatchCommand();\n }\n\n // Update layer containers and current_layer.\n const index = this.all_layers.indexOf(this.current_layer);\n if (index >= 0) {\n this.all_layers.splice(index + 1, 0, layer);\n } else {\n this.all_layers.push(layer);\n }\n this.layer_map[name] = layer;\n this.current_layer = layer;\n return group;\n }\n\n /**\n * Returns whether the layer is visible. If the layer name is not valid,\n * then this function returns false.\n * @param {string} layername - The name of the layer which you want to query.\n * @returns {boolean} The visibility state of the layer, or false if the layer name was invalid.\n */\n getLayerVisibility (layername) {\n const layer = this.layer_map[layername];\n return layer ? layer.isVisible() : false;\n }\n\n /**\n * Sets the visibility of the layer. If the layer name is not valid, this\n * function returns false, otherwise it returns true. This is an\n * undo-able action.\n * @param {string} layername - The name of the layer to change the visibility\n * @param {boolean} bVisible - Whether the layer should be visible\n * @returns {?SVGGElement} The SVGGElement representing the layer if the\n * layername was valid, otherwise null.\n */\n setLayerVisibility (layername, bVisible) {\n if (typeof bVisible !== 'boolean') {\n return null;\n }\n const layer = this.layer_map[layername];\n if (!layer) { return null; }\n layer.setVisible(bVisible);\n return layer.getGroup();\n }\n\n /**\n * Returns the opacity of the given layer. If the input name is not a layer, null is returned.\n * @param {string} layername - name of the layer on which to get the opacity\n * @returns {?number} The opacity value of the given layer. This will be a value between 0.0 and 1.0, or null\n * if layername is not a valid layer\n */\n getLayerOpacity (layername) {\n const layer = this.layer_map[layername];\n if (!layer) { return null; }\n return layer.getOpacity();\n }\n\n /**\n * Sets the opacity of the given layer. If the input name is not a layer,\n * nothing happens. If opacity is not a value between 0.0 and 1.0, then\n * nothing happens.\n * @param {string} layername - Name of the layer on which to set the opacity\n * @param {number} opacity - A float value in the range 0.0-1.0\n */\n setLayerOpacity (layername, opacity) {\n if (typeof opacity !== 'number' || opacity < 0.0 || opacity > 1.0) {\n return;\n }\n const layer = this.layer_map[layername];\n if (layer) {\n layer.setOpacity(opacity);\n }\n }\n\n /**\n * Create a clone of an element, updating its ID and its children's IDs when needed\n * @param {Element} el - DOM element to clone\n * @returns {Element}\n */\n copyElem (el) {\n const self = this;\n const getNextIdClosure = function () { return self.getNextId(); };\n return utilCopyElem(el, getNextIdClosure);\n }\n}\n\n/**\n * Called to ensure that drawings will or will not have randomized ids.\n * The currentDrawing will have its nonce set if it doesn't already.\n * @param {boolean} enableRandomization - flag indicating if documents should have randomized ids\n * @param {svgedit.draw.Drawing} currentDrawing\n */\nexport const randomizeIds = function (enableRandomization, currentDrawing) {\n randIds = enableRandomization === false\n ? RandomizeModes.NEVER_RANDOMIZE\n : RandomizeModes.ALWAYS_RANDOMIZE;\n\n if (randIds === RandomizeModes.ALWAYS_RANDOMIZE && !currentDrawing.getNonce()) {\n currentDrawing.setNonce(Math.floor(Math.random() * 100001));\n } else if (randIds === RandomizeModes.NEVER_RANDOMIZE && currentDrawing.getNonce()) {\n currentDrawing.clearNonce();\n }\n};\n\n// Layer API Functions\n\n/**\n* Group: Layers\n*/\n\nlet canvas_;\nexport const init = function (canvas) {\n canvas_ = canvas;\n};\n\n// Updates layer system\nexport const identifyLayers = function () {\n leaveContext();\n canvas_.getCurrentDrawing().identifyLayers();\n};\n\n/**\n* Creates a new top-level layer in the drawing with the given name, sets the current layer\n* to it, and then clears the selection. This function then calls the 'changed' handler.\n* This is an undoable action.\n* @param name - The given name\n* @param hrService\n*/\nexport const createLayer = function (name, hrService) {\n const newLayer = canvas_.getCurrentDrawing().createLayer(\n name,\n historyRecordingService(hrService)\n );\n canvas_.clearSelection();\n canvas_.call('changed', [newLayer]);\n};\n\n/**\n * Creates a new top-level layer in the drawing with the given name, copies all the current layer's contents\n * to it, and then clears the selection. This function then calls the 'changed' handler.\n * This is an undoable action.\n * @param {string} name - The given name. If the layer name exists, a new name will be generated.\n * @param {svgedit.history.HistoryRecordingService} hrService - History recording service\n */\nexport const cloneLayer = function (name, hrService) {\n // Clone the current layer and make the cloned layer the new current layer\n const newLayer = canvas_.getCurrentDrawing().cloneLayer(name, historyRecordingService(hrService));\n\n canvas_.clearSelection();\n leaveContext();\n canvas_.call('changed', [newLayer]);\n};\n\n/**\n* Deletes the current layer from the drawing and then clears the selection. This function\n* then calls the 'changed' handler. This is an undoable action.\n*/\nexport const deleteCurrentLayer = function () {\n let currentLayer = canvas_.getCurrentDrawing().getCurrentLayer();\n const {nextSibling} = currentLayer;\n const parent = currentLayer.parentNode;\n currentLayer = canvas_.getCurrentDrawing().deleteCurrentLayer();\n if (currentLayer) {\n const batchCmd = new BatchCommand('Delete Layer');\n // store in our Undo History\n batchCmd.addSubCommand(new RemoveElementCommand(currentLayer, nextSibling, parent));\n canvas_.addCommandToHistory(batchCmd);\n canvas_.clearSelection();\n canvas_.call('changed', [parent]);\n return true;\n }\n return false;\n};\n\n/**\n* Sets the current layer. If the name is not a valid layer name, then this function returns\n* false. Otherwise it returns true. This is not an undo-able action.\n* @param name - The name of the layer you want to switch to.\n*\n* @returns true if the current layer was switched, otherwise false\n*/\nexport const setCurrentLayer = function (name) {\n const result = canvas_.getCurrentDrawing().setCurrentLayer(toXml(name));\n if (result) {\n canvas_.clearSelection();\n }\n return result;\n};\n\n/**\n* Renames the current layer. If the layer name is not valid (i.e. unique), then this function\n* does nothing and returns false, otherwise it returns true. This is an undo-able action.\n*\n* @param newname - the new name you want to give the current layer. This name must be unique\n* among all layer names.\n* @returns {Boolean} Whether the rename succeeded\n*/\nexport const renameCurrentLayer = function (newname) {\n const drawing = canvas_.getCurrentDrawing();\n const layer = drawing.getCurrentLayer();\n if (layer) {\n const result = drawing.setCurrentLayerName(newname, historyRecordingService());\n if (result) {\n canvas_.call('changed', [layer]);\n return true;\n }\n }\n return false;\n};\n\n/**\n* Changes the position of the current layer to the new value. If the new index is not valid,\n* this function does nothing and returns false, otherwise it returns true. This is an\n* undo-able action.\n* @param newpos - The zero-based index of the new position of the layer. This should be between\n* 0 and (number of layers - 1)\n*\n* @returns {Boolean} true if the current layer position was changed, false otherwise.\n*/\nexport const setCurrentLayerPosition = function (newpos) {\n const drawing = canvas_.getCurrentDrawing();\n const result = drawing.setCurrentLayerPosition(newpos);\n if (result) {\n canvas_.addCommandToHistory(new MoveElementCommand(result.currentGroup, result.oldNextSibling, canvas_.getSVGContent()));\n return true;\n }\n return false;\n};\n\n/**\n* Sets the visibility of the layer. If the layer name is not valid, this function return\n* false, otherwise it returns true. This is an undo-able action.\n* @param layername - The name of the layer to change the visibility\n* @param {Boolean} bVisible - Whether the layer should be visible\n* @returns {Boolean} true if the layer's visibility was set, false otherwise\n*/\nexport const setLayerVisibility = function (layername, bVisible) {\n const drawing = canvas_.getCurrentDrawing();\n const prevVisibility = drawing.getLayerVisibility(layername);\n const layer = drawing.setLayerVisibility(layername, bVisible);\n if (layer) {\n const oldDisplay = prevVisibility ? 'inline' : 'none';\n canvas_.addCommandToHistory(new ChangeElementCommand(layer, {display: oldDisplay}, 'Layer Visibility'));\n } else {\n return false;\n }\n\n if (layer === drawing.getCurrentLayer()) {\n canvas_.clearSelection();\n canvas_.pathActions.clear();\n }\n // call('changed', [selected]);\n return true;\n};\n\n/**\n* Moves the selected elements to layername. If the name is not a valid layer name, then false\n* is returned. Otherwise it returns true. This is an undo-able action.\n*\n* @param layername - The name of the layer you want to which you want to move the selected elements\n* @returns {Boolean} Whether the selected elements were moved to the layer.\n*/\nexport const moveSelectedToLayer = function (layername) {\n // find the layer\n const drawing = canvas_.getCurrentDrawing();\n const layer = drawing.getLayerByName(layername);\n if (!layer) { return false; }\n\n const batchCmd = new BatchCommand('Move Elements to Layer');\n\n // loop for each selected element and move it\n const selElems = canvas_.getSelectedElements();\n let i = selElems.length;\n while (i--) {\n const elem = selElems[i];\n if (!elem) { continue; }\n const oldNextSibling = elem.nextSibling;\n // TODO: this is pretty brittle!\n const oldLayer = elem.parentNode;\n layer.appendChild(elem);\n batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldLayer));\n }\n\n canvas_.addCommandToHistory(batchCmd);\n\n return true;\n};\n\nexport const mergeLayer = function (hrService) {\n canvas_.getCurrentDrawing().mergeLayer(historyRecordingService(hrService));\n canvas_.clearSelection();\n leaveContext();\n canvas_.changeSvgcontent();\n};\n\nexport const mergeAllLayers = function (hrService) {\n canvas_.getCurrentDrawing().mergeAllLayers(historyRecordingService(hrService));\n canvas_.clearSelection();\n leaveContext();\n canvas_.changeSvgcontent();\n};\n\n// Return from a group context to the regular kind, make any previously\n// disabled elements enabled again\nexport const leaveContext = function () {\n const len = disabledElems.length;\n if (len) {\n for (let i = 0; i < len; i++) {\n const elem = disabledElems[i];\n const orig = canvas_.elData(elem, 'orig_opac');\n if (orig !== 1) {\n elem.setAttribute('opacity', orig);\n } else {\n elem.removeAttribute('opacity');\n }\n elem.setAttribute('style', 'pointer-events: inherit');\n }\n disabledElems = [];\n canvas_.clearSelection(true);\n canvas_.call('contextset', null);\n }\n canvas_.setCurrentGroup(null);\n};\n\n// Set the current context (for in-group editing)\nexport const setContext = function (elem) {\n leaveContext();\n if (typeof elem === 'string') {\n elem = getElem(elem);\n }\n\n // Edit inside this group\n canvas_.setCurrentGroup(elem);\n\n // Disable other elements\n $(elem).parentsUntil('#svgcontent').andSelf().siblings().each(function () {\n const opac = this.getAttribute('opacity') || 1;\n // Store the original's opacity\n canvas_.elData(this, 'orig_opac', opac);\n this.setAttribute('opacity', opac * 0.33);\n this.setAttribute('style', 'pointer-events: none');\n disabledElems.push(this);\n });\n\n canvas_.clearSelection();\n canvas_.call('contextset', canvas_.getCurrentGroup());\n};\n\nexport {Layer};\n","/**\n * Package: svgedit.sanitize\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Alexis Deveria\n * Copyright(c) 2010 Jeff Schiller\n */\n\nimport {getReverseNS, NS} from './svgedit.js';\nimport {isGecko} from './browser.js';\nimport {getHref, setHref, getUrlFromAttr} from './svgutils.js';\n\nconst REVERSE_NS = getReverseNS();\n\n// this defines which elements and attributes that we support\nconst svgWhiteList_ = {\n // SVG Elements\n a: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'xlink:href', 'xlink:title'],\n circle: ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'r', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],\n clipPath: ['class', 'clipPathUnits', 'id'],\n defs: [],\n style: ['type'],\n desc: [],\n ellipse: ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],\n feGaussianBlur: ['class', 'color-interpolation-filters', 'id', 'requiredFeatures', 'stdDeviation'],\n filter: ['class', 'color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'id', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],\n foreignObject: ['class', 'font-size', 'height', 'id', 'opacity', 'requiredFeatures', 'style', 'transform', 'width', 'x', 'y'],\n g: ['class', 'clip-path', 'clip-rule', 'id', 'display', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'font-family', 'font-size', 'font-style', 'font-weight', 'text-anchor'],\n image: ['class', 'clip-path', 'clip-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'style', 'systemLanguage', 'transform', 'width', 'x', 'xlink:href', 'xlink:title', 'y'],\n line: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'x1', 'x2', 'y1', 'y2'],\n linearGradient: ['class', 'id', 'gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2'],\n marker: ['id', 'class', 'markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'systemLanguage', 'viewBox'],\n mask: ['class', 'height', 'id', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y'],\n metadata: ['class', 'id'],\n path: ['class', 'clip-path', 'clip-rule', 'd', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],\n pattern: ['class', 'height', 'id', 'patternContentUnits', 'patternTransform', 'patternUnits', 'requiredFeatures', 'style', 'systemLanguage', 'viewBox', 'width', 'x', 'xlink:href', 'y'],\n polygon: ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'class', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],\n polyline: ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],\n radialGradient: ['class', 'cx', 'cy', 'fx', 'fy', 'gradientTransform', 'gradientUnits', 'id', 'r', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'xlink:href'],\n rect: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'width', 'x', 'y'],\n stop: ['class', 'id', 'offset', 'requiredFeatures', 'stop-color', 'stop-opacity', 'style', 'systemLanguage'],\n svg: ['class', 'clip-path', 'clip-rule', 'filter', 'id', 'height', 'mask', 'preserveAspectRatio', 'requiredFeatures', 'style', 'systemLanguage', 'viewBox', 'width', 'x', 'xmlns', 'xmlns:se', 'xmlns:xlink', 'y'],\n switch: ['class', 'id', 'requiredFeatures', 'systemLanguage'],\n symbol: ['class', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'opacity', 'preserveAspectRatio', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'viewBox'],\n text: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'text-anchor', 'transform', 'x', 'xml:space', 'y'],\n textPath: ['class', 'id', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'style', 'systemLanguage', 'transform', 'xlink:href'],\n title: [],\n tspan: ['class', 'clip-path', 'clip-rule', 'dx', 'dy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'text-anchor', 'textLength', 'transform', 'x', 'xml:space', 'y'],\n use: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'id', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'transform', 'width', 'x', 'xlink:href', 'y'],\n\n // MathML Elements\n annotation: ['encoding'],\n 'annotation-xml': ['encoding'],\n maction: ['actiontype', 'other', 'selection'],\n math: ['class', 'id', 'display', 'xmlns'],\n menclose: ['notation'],\n merror: [],\n mfrac: ['linethickness'],\n mi: ['mathvariant'],\n mmultiscripts: [],\n mn: [],\n mo: ['fence', 'lspace', 'maxsize', 'minsize', 'rspace', 'stretchy'],\n mover: [],\n mpadded: ['lspace', 'width', 'height', 'depth', 'voffset'],\n mphantom: [],\n mprescripts: [],\n mroot: [],\n mrow: ['xlink:href', 'xlink:type', 'xmlns:xlink'],\n mspace: ['depth', 'height', 'width'],\n msqrt: [],\n mstyle: ['displaystyle', 'mathbackground', 'mathcolor', 'mathvariant', 'scriptlevel'],\n msub: [],\n msubsup: [],\n msup: [],\n mtable: ['align', 'columnalign', 'columnlines', 'columnspacing', 'displaystyle', 'equalcolumns', 'equalrows', 'frame', 'rowalign', 'rowlines', 'rowspacing', 'width'],\n mtd: ['columnalign', 'columnspan', 'rowalign', 'rowspan'],\n mtext: [],\n mtr: ['columnalign', 'rowalign'],\n munder: [],\n munderover: [],\n none: [],\n semantics: []\n};\n\n// Produce a Namespace-aware version of svgWhitelist\nconst svgWhiteListNS_ = {};\nObject.entries(svgWhiteList_).forEach(function ([elt, atts]) {\n const attNS = {};\n Object.entries(atts).forEach(function ([i, att]) {\n if (att.includes(':')) {\n const v = att.split(':');\n attNS[v[1]] = NS[(v[0]).toUpperCase()];\n } else {\n attNS[att] = att === 'xmlns' ? NS.XMLNS : null;\n }\n });\n svgWhiteListNS_[elt] = attNS;\n});\n\n/**\n* Sanitizes the input node and its children\n* It only keeps what is allowed from our whitelist defined above\n* @param node - The DOM element to be checked (we'll also check its children)\n*/\nexport const sanitizeSvg = function (node) {\n // Cleanup text nodes\n if (node.nodeType === 3) { // 3 === TEXT_NODE\n // Trim whitespace\n node.nodeValue = node.nodeValue.replace(/^\\s+|\\s+$/g, '');\n // Remove if empty\n if (!node.nodeValue.length) {\n node.parentNode.removeChild(node);\n }\n }\n\n // We only care about element nodes.\n // Automatically return for all non-element nodes, such as comments, etc.\n if (node.nodeType !== 1) { // 1 == ELEMENT_NODE\n return;\n }\n\n const doc = node.ownerDocument;\n const parent = node.parentNode;\n // can parent ever be null here? I think the root node's parent is the document...\n if (!doc || !parent) {\n return;\n }\n\n const allowedAttrs = svgWhiteList_[node.nodeName];\n const allowedAttrsNS = svgWhiteListNS_[node.nodeName];\n // if this element is supported, sanitize it\n if (typeof allowedAttrs !== 'undefined') {\n const seAttrs = [];\n let i = node.attributes.length;\n while (i--) {\n // if the attribute is not in our whitelist, then remove it\n // could use jQuery's inArray(), but I don't know if that's any better\n const attr = node.attributes.item(i);\n const attrName = attr.nodeName;\n const attrLocalName = attr.localName;\n const attrNsURI = attr.namespaceURI;\n // Check that an attribute with the correct localName in the correct namespace is on\n // our whitelist or is a namespace declaration for one of our allowed namespaces\n if (!(allowedAttrsNS.hasOwnProperty(attrLocalName) && attrNsURI === allowedAttrsNS[attrLocalName] && attrNsURI !== NS.XMLNS) &&\n !(attrNsURI === NS.XMLNS && REVERSE_NS[attr.value])) {\n // TODO(codedread): Programmatically add the se: attributes to the NS-aware whitelist.\n // Bypassing the whitelist to allow se: prefixes.\n // Is there a more appropriate way to do this?\n if (attrName.startsWith('se:') || attrName.startsWith('data-')) {\n seAttrs.push([attrName, attr.value]);\n }\n node.removeAttributeNS(attrNsURI, attrLocalName);\n }\n\n // Add spaces before negative signs where necessary\n if (isGecko()) {\n switch (attrName) {\n case 'transform':\n case 'gradientTransform':\n case 'patternTransform':\n const val = attr.value.replace(/(\\d)-/g, '$1 -');\n node.setAttribute(attrName, val);\n break;\n }\n }\n\n // For the style attribute, rewrite it in terms of XML presentational attributes\n if (attrName === 'style') {\n const props = attr.value.split(';');\n let p = props.length;\n while (p--) {\n const [name, val] = props[p].split(':');\n const styleAttrName = (name || '').trim();\n const styleAttrVal = (val || '').trim();\n // Now check that this attribute is supported\n if (allowedAttrs.includes(styleAttrName)) {\n node.setAttribute(styleAttrName, styleAttrVal);\n }\n }\n node.removeAttribute('style');\n }\n }\n\n Object.values(seAttrs).forEach(function (attr) {\n node.setAttributeNS(NS.SE, attr[0], attr[1]);\n });\n\n // for some elements that have a xlink:href, ensure the URI refers to a local element\n // (but not for links)\n const href = getHref(node);\n if (href &&\n ['filter', 'linearGradient', 'pattern',\n 'radialGradient', 'textPath', 'use'].includes(node.nodeName)) {\n // TODO: we simply check if the first character is a #, is this bullet-proof?\n if (href[0] !== '#') {\n // remove the attribute (but keep the element)\n setHref(node, '');\n node.removeAttributeNS(NS.XLINK, 'href');\n }\n }\n\n // Safari crashes on a without a xlink:href, so we just remove the node here\n if (node.nodeName === 'use' && !getHref(node)) {\n parent.removeChild(node);\n return;\n }\n // if the element has attributes pointing to a non-local reference,\n // need to remove the attribute\n Object.values(['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'], function (attr) {\n let val = node.getAttribute(attr);\n if (val) {\n val = getUrlFromAttr(val);\n // simply check for first character being a '#'\n if (val && val[0] !== '#') {\n node.setAttribute(attr, '');\n node.removeAttribute(attr);\n }\n }\n });\n\n // recurse to children\n i = node.childNodes.length;\n while (i--) { sanitizeSvg(node.childNodes.item(i)); }\n // else (element not supported), remove it\n } else {\n // remove all children from this node and insert them before this node\n // FIXME: in the case of animation elements this will hardly ever be correct\n const children = [];\n while (node.hasChildNodes()) {\n children.push(parent.insertBefore(node.firstChild, node));\n }\n\n // remove this node from the document altogether\n parent.removeChild(node);\n\n // call sanitizeSvg on each of those children\n let i = children.length;\n while (i--) { sanitizeSvg(children[i]); }\n }\n};\n","/* globals jQuery */\n/**\n * Coords.\n *\n * Licensed under the MIT License\n *\n */\n\nimport './pathseg.js';\nimport {\n snapToGrid, assignAttributes, getBBox, getRefElem, findDefs\n} from './svgutils.js';\nimport {\n transformPoint, transformListToTransform, matrixMultiply, transformBox\n} from './math.js';\nimport {getTransformList} from './svgtransformlist.js';\n\nconst $ = jQuery;\n\n// this is how we map paths to our preferred relative segment types\nconst pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',\n 'H', 'h', 'V', 'v', 'S', 's', 'T', 't'];\n\n/**\n * @typedef editorContext\n * @type {?object}\n * @property {function} getGridSnapping\n * @property {function} getDrawing\n*/\nlet editorContext_ = null;\n\n/**\n* @param {editorContext} editorContext\n*/\nexport const init = function (editorContext) {\n editorContext_ = editorContext;\n};\n\n/**\n * Applies coordinate changes to an element based on the given matrix\n * @param {Element} selected - DOM element to be changed\n * @param {Object} changes - Object with changes to be remapped\n * @param {SVGMatrix} m - Matrix object to use for remapping coordinates\n*/\nexport const remapElement = function (selected, changes, m) {\n const remap = function (x, y) { return transformPoint(x, y, m); },\n scalew = function (w) { return m.a * w; },\n scaleh = function (h) { return m.d * h; },\n doSnapping = editorContext_.getGridSnapping() && selected.parentNode.parentNode.localName === 'svg',\n finishUp = function () {\n if (doSnapping) {\n for (const o in changes) {\n changes[o] = snapToGrid(changes[o]);\n }\n }\n assignAttributes(selected, changes, 1000, true);\n },\n box = getBBox(selected);\n\n for (let i = 0; i < 2; i++) {\n const type = i === 0 ? 'fill' : 'stroke';\n const attrVal = selected.getAttribute(type);\n if (attrVal && attrVal.startsWith('url(')) {\n if (m.a < 0 || m.d < 0) {\n const grad = getRefElem(attrVal);\n const newgrad = grad.cloneNode(true);\n if (m.a < 0) {\n // flip x\n const x1 = newgrad.getAttribute('x1');\n const x2 = newgrad.getAttribute('x2');\n newgrad.setAttribute('x1', -(x1 - 1));\n newgrad.setAttribute('x2', -(x2 - 1));\n }\n\n if (m.d < 0) {\n // flip y\n const y1 = newgrad.getAttribute('y1');\n const y2 = newgrad.getAttribute('y2');\n newgrad.setAttribute('y1', -(y1 - 1));\n newgrad.setAttribute('y2', -(y2 - 1));\n }\n newgrad.id = editorContext_.getDrawing().getNextId();\n findDefs().appendChild(newgrad);\n selected.setAttribute(type, 'url(#' + newgrad.id + ')');\n }\n\n // Not really working :(\n // if (selected.tagName === 'path') {\n // reorientGrads(selected, m);\n // }\n }\n }\n\n const elName = selected.tagName;\n if (elName === 'g' || elName === 'text' || elName === 'tspan' || elName === 'use') {\n // if it was a translate, then just update x,y\n if (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && (m.e !== 0 || m.f !== 0)) {\n // [T][M] = [M][T']\n // therefore [T'] = [M_inv][T][M]\n const existing = transformListToTransform(selected).matrix,\n tNew = matrixMultiply(existing.inverse(), m, existing);\n changes.x = parseFloat(changes.x) + tNew.e;\n changes.y = parseFloat(changes.y) + tNew.f;\n } else {\n // we just absorb all matrices into the element and don't do any remapping\n const chlist = getTransformList(selected);\n const mt = editorContext_.getSVGRoot().createSVGTransform();\n mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m));\n chlist.clear();\n chlist.appendItem(mt);\n }\n }\n\n // now we have a set of changes and an applied reduced transform list\n // we apply the changes directly to the DOM\n switch (elName) {\n case 'foreignObject':\n case 'rect':\n case 'image': {\n // Allow images to be inverted (give them matrix when flipped)\n if (elName === 'image' && (m.a < 0 || m.d < 0)) {\n // Convert to matrix\n const chlist = getTransformList(selected);\n const mt = editorContext_.getSVGRoot().createSVGTransform();\n mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m));\n chlist.clear();\n chlist.appendItem(mt);\n } else {\n const pt1 = remap(changes.x, changes.y);\n changes.width = scalew(changes.width);\n changes.height = scaleh(changes.height);\n changes.x = pt1.x + Math.min(0, changes.width);\n changes.y = pt1.y + Math.min(0, changes.height);\n changes.width = Math.abs(changes.width);\n changes.height = Math.abs(changes.height);\n }\n finishUp();\n break;\n } case 'ellipse': {\n const c = remap(changes.cx, changes.cy);\n changes.cx = c.x;\n changes.cy = c.y;\n changes.rx = scalew(changes.rx);\n changes.ry = scaleh(changes.ry);\n changes.rx = Math.abs(changes.rx);\n changes.ry = Math.abs(changes.ry);\n finishUp();\n break;\n } case 'circle': {\n const c = remap(changes.cx, changes.cy);\n changes.cx = c.x;\n changes.cy = c.y;\n // take the minimum of the new selected box's dimensions for the new circle radius\n const tbox = transformBox(box.x, box.y, box.width, box.height, m);\n const w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y;\n changes.r = Math.min(w / 2, h / 2);\n\n if (changes.r) { changes.r = Math.abs(changes.r); }\n finishUp();\n break;\n } case 'line': {\n const pt1 = remap(changes.x1, changes.y1);\n const pt2 = remap(changes.x2, changes.y2);\n changes.x1 = pt1.x;\n changes.y1 = pt1.y;\n changes.x2 = pt2.x;\n changes.y2 = pt2.y;\n } // Fallthrough\n case 'text':\n case 'tspan':\n case 'use': {\n finishUp();\n break;\n } case 'g': {\n const gsvg = $(selected).data('gsvg');\n if (gsvg) {\n assignAttributes(gsvg, changes, 1000, true);\n }\n break;\n } case 'polyline':\n case 'polygon': {\n const len = changes.points.length;\n for (let i = 0; i < len; ++i) {\n const pt = changes.points[i];\n const {x, y} = remap(pt.x, pt.y);\n changes.points[i].x = x;\n changes.points[i].y = y;\n }\n\n // const len = changes.points.length;\n let pstr = '';\n for (let i = 0; i < len; ++i) {\n const pt = changes.points[i];\n pstr += pt.x + ',' + pt.y + ' ';\n }\n selected.setAttribute('points', pstr);\n break;\n } case 'path': {\n const segList = selected.pathSegList;\n let len = segList.numberOfItems;\n changes.d = [];\n for (let i = 0; i < len; ++i) {\n const seg = segList.getItem(i);\n changes.d[i] = {\n type: seg.pathSegType,\n x: seg.x,\n y: seg.y,\n x1: seg.x1,\n y1: seg.y1,\n x2: seg.x2,\n y2: seg.y2,\n r1: seg.r1,\n r2: seg.r2,\n angle: seg.angle,\n largeArcFlag: seg.largeArcFlag,\n sweepFlag: seg.sweepFlag\n };\n }\n\n len = changes.d.length;\n const firstseg = changes.d[0],\n currentpt = remap(firstseg.x, firstseg.y);\n changes.d[0].x = currentpt.x;\n changes.d[0].y = currentpt.y;\n for (let i = 1; i < len; ++i) {\n const seg = changes.d[i];\n const {type} = seg;\n // if absolute or first segment, we want to remap x, y, x1, y1, x2, y2\n // if relative, we want to scalew, scaleh\n if (type % 2 === 0) { // absolute\n const thisx = (seg.x !== undefined) ? seg.x : currentpt.x, // for V commands\n thisy = (seg.y !== undefined) ? seg.y : currentpt.y; // for H commands\n const pt = remap(thisx, thisy);\n const pt1 = remap(seg.x1, seg.y1);\n const pt2 = remap(seg.x2, seg.y2);\n seg.x = pt.x;\n seg.y = pt.y;\n seg.x1 = pt1.x;\n seg.y1 = pt1.y;\n seg.x2 = pt2.x;\n seg.y2 = pt2.y;\n seg.r1 = scalew(seg.r1);\n seg.r2 = scaleh(seg.r2);\n } else { // relative\n seg.x = scalew(seg.x);\n seg.y = scaleh(seg.y);\n seg.x1 = scalew(seg.x1);\n seg.y1 = scaleh(seg.y1);\n seg.x2 = scalew(seg.x2);\n seg.y2 = scaleh(seg.y2);\n seg.r1 = scalew(seg.r1);\n seg.r2 = scaleh(seg.r2);\n }\n } // for each segment\n\n let dstr = '';\n len = changes.d.length;\n for (let i = 0; i < len; ++i) {\n const seg = changes.d[i];\n const {type} = seg;\n dstr += pathMap[type];\n switch (type) {\n case 13: // relative horizontal line (h)\n case 12: // absolute horizontal line (H)\n dstr += seg.x + ' ';\n break;\n case 15: // relative vertical line (v)\n case 14: // absolute vertical line (V)\n dstr += seg.y + ' ';\n break;\n case 3: // relative move (m)\n case 5: // relative line (l)\n case 19: // relative smooth quad (t)\n case 2: // absolute move (M)\n case 4: // absolute line (L)\n case 18: // absolute smooth quad (T)\n dstr += seg.x + ',' + seg.y + ' ';\n break;\n case 7: // relative cubic (c)\n case 6: // absolute cubic (C)\n dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x2 + ',' + seg.y2 + ' ' +\n seg.x + ',' + seg.y + ' ';\n break;\n case 9: // relative quad (q)\n case 8: // absolute quad (Q)\n dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x + ',' + seg.y + ' ';\n break;\n case 11: // relative elliptical arc (a)\n case 10: // absolute elliptical arc (A)\n dstr += seg.r1 + ',' + seg.r2 + ' ' + seg.angle + ' ' + (+seg.largeArcFlag) +\n ' ' + (+seg.sweepFlag) + ' ' + seg.x + ',' + seg.y + ' ';\n break;\n case 17: // relative smooth cubic (s)\n case 16: // absolute smooth cubic (S)\n dstr += seg.x2 + ',' + seg.y2 + ' ' + seg.x + ',' + seg.y + ' ';\n break;\n }\n }\n\n selected.setAttribute('d', dstr);\n break;\n }\n }\n};\n","/* globals jQuery */\n/**\n * Recalculate.\n *\n * Licensed under the MIT License\n *\n */\n\n// Dependencies:\n// 1) jquery-svg.js\n\nimport jqPluginSVG from './jquery-svg.js'; // Needed for SVG attribute setting and array form with `attr`\nimport {NS} from './svgedit.js';\nimport {convertToNum} from './units.js';\nimport {isWebkit} from './browser.js';\nimport {getTransformList} from './svgtransformlist.js';\nimport {getRotationAngle, getHref, getBBox, getRefElem} from './svgutils.js';\nimport {BatchCommand, ChangeElementCommand} from './history.js';\nimport {remapElement} from './coords.js';\nimport {\n isIdentity, matrixMultiply, transformPoint, transformListToTransform,\n hasMatrixTransform\n} from './math.js';\n\nconst $ = jqPluginSVG(jQuery);\n\nlet context_;\n\n/**\n* @param editorContext\n*/\nexport const init = function (editorContext) {\n context_ = editorContext;\n};\n\n/**\n* Updates a s values based on the given translation of an element\n* @param attr - The clip-path attribute value with the clipPath's ID\n* @param tx - The translation's x value\n* @param ty - The translation's y value\n*/\nexport const updateClipPath = function (attr, tx, ty) {\n const path = getRefElem(attr).firstChild;\n const cpXform = getTransformList(path);\n const newxlate = context_.getSVGRoot().createSVGTransform();\n newxlate.setTranslate(tx, ty);\n\n cpXform.appendItem(newxlate);\n\n // Update clipPath's dimensions\n recalculateDimensions(path);\n};\n\n/**\n* Decides the course of action based on the element's transform list\n* @param selected - The DOM element to recalculate\n* @returns Undo command object with the resulting change\n*/\nexport const recalculateDimensions = function (selected) {\n if (selected == null) { return null; }\n\n // Firefox Issue - 1081\n if (selected.nodeName === 'svg' && navigator.userAgent.includes('Firefox/20')) {\n return null;\n }\n\n const svgroot = context_.getSVGRoot();\n const tlist = getTransformList(selected);\n\n // remove any unnecessary transforms\n if (tlist && tlist.numberOfItems > 0) {\n let k = tlist.numberOfItems;\n const noi = k;\n while (k--) {\n const xform = tlist.getItem(k);\n if (xform.type === 0) {\n tlist.removeItem(k);\n // remove identity matrices\n } else if (xform.type === 1) {\n if (isIdentity(xform.matrix)) {\n if (noi === 1) {\n // Overcome Chrome bug (though only when noi is 1) with\n // `removeItem` preventing `removeAttribute` from\n // subsequently working\n // See https://bugs.chromium.org/p/chromium/issues/detail?id=843901\n selected.removeAttribute('transform');\n return null;\n }\n tlist.removeItem(k);\n }\n // remove zero-degree rotations\n } else if (xform.type === 4) {\n if (xform.angle === 0) {\n tlist.removeItem(k);\n }\n }\n }\n // End here if all it has is a rotation\n if (tlist.numberOfItems === 1 &&\n getRotationAngle(selected)) { return null; }\n }\n\n // if this element had no transforms, we are done\n if (!tlist || tlist.numberOfItems === 0) {\n // Chrome apparently had a bug that requires clearing the attribute first.\n selected.setAttribute('transform', '');\n // However, this still next line currently doesn't work at all in Chrome\n selected.removeAttribute('transform');\n // selected.transform.baseVal.clear(); // Didn't help for Chrome bug\n return null;\n }\n\n // TODO: Make this work for more than 2\n if (tlist) {\n let mxs = [];\n let k = tlist.numberOfItems;\n while (k--) {\n const xform = tlist.getItem(k);\n if (xform.type === 1) {\n mxs.push([xform.matrix, k]);\n } else if (mxs.length) {\n mxs = [];\n }\n }\n if (mxs.length === 2) {\n const mNew = svgroot.createSVGTransformFromMatrix(matrixMultiply(mxs[1][0], mxs[0][0]));\n tlist.removeItem(mxs[0][1]);\n tlist.removeItem(mxs[1][1]);\n tlist.insertItemBefore(mNew, mxs[1][1]);\n }\n\n // combine matrix + translate\n k = tlist.numberOfItems;\n if (k >= 2 && tlist.getItem(k - 2).type === 1 && tlist.getItem(k - 1).type === 2) {\n const mt = svgroot.createSVGTransform();\n\n const m = matrixMultiply(\n tlist.getItem(k - 2).matrix,\n tlist.getItem(k - 1).matrix);\n mt.setMatrix(m);\n tlist.removeItem(k - 2);\n tlist.removeItem(k - 2);\n tlist.appendItem(mt);\n }\n }\n\n // If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned).\n switch (selected.tagName) {\n // Ignore these elements, as they can absorb the [M]\n case 'line':\n case 'polyline':\n case 'polygon':\n case 'path':\n break;\n default:\n if ((tlist.numberOfItems === 1 && tlist.getItem(0).type === 1) ||\n (tlist.numberOfItems === 2 && tlist.getItem(0).type === 1 && tlist.getItem(0).type === 4)) {\n return null;\n }\n }\n\n // Grouped SVG element\n const gsvg = $(selected).data('gsvg');\n\n // we know we have some transforms, so set up return variable\n const batchCmd = new BatchCommand('Transform');\n\n // store initial values that will be affected by reducing the transform list\n let changes = {};\n let initial = null;\n let attrs = [];\n switch (selected.tagName) {\n case 'line':\n attrs = ['x1', 'y1', 'x2', 'y2'];\n break;\n case 'circle':\n attrs = ['cx', 'cy', 'r'];\n break;\n case 'ellipse':\n attrs = ['cx', 'cy', 'rx', 'ry'];\n break;\n case 'foreignObject':\n case 'rect':\n case 'image':\n attrs = ['width', 'height', 'x', 'y'];\n break;\n case 'use':\n case 'text':\n case 'tspan':\n attrs = ['x', 'y'];\n break;\n case 'polygon':\n case 'polyline': {\n initial = {};\n initial.points = selected.getAttribute('points');\n const list = selected.points;\n const len = list.numberOfItems;\n changes.points = new Array(len);\n for (let i = 0; i < len; ++i) {\n const pt = list.getItem(i);\n changes.points[i] = {x: pt.x, y: pt.y};\n }\n break;\n } case 'path':\n initial = {};\n initial.d = selected.getAttribute('d');\n changes.d = selected.getAttribute('d');\n break;\n } // switch on element type to get initial values\n\n if (attrs.length) {\n changes = $(selected).attr(attrs);\n $.each(changes, function (attr, val) {\n changes[attr] = convertToNum(attr, val);\n });\n } else if (gsvg) {\n // GSVG exception\n changes = {\n x: $(gsvg).attr('x') || 0,\n y: $(gsvg).attr('y') || 0\n };\n }\n\n // if we haven't created an initial array in polygon/polyline/path, then\n // make a copy of initial values and include the transform\n if (initial == null) {\n initial = $.extend(true, {}, changes);\n $.each(initial, function (attr, val) {\n initial[attr] = convertToNum(attr, val);\n });\n }\n // save the start transform value too\n initial.transform = context_.getStartTransform() || '';\n\n let oldcenter, newcenter;\n\n // if it's a regular group, we have special processing to flatten transforms\n if ((selected.tagName === 'g' && !gsvg) || selected.tagName === 'a') {\n const box = getBBox(selected);\n\n oldcenter = {x: box.x + box.width / 2, y: box.y + box.height / 2};\n newcenter = transformPoint(\n box.x + box.width / 2,\n box.y + box.height / 2,\n transformListToTransform(tlist).matrix\n );\n let m = svgroot.createSVGMatrix();\n\n // temporarily strip off the rotate and save the old center\n const gangle = getRotationAngle(selected);\n if (gangle) {\n const a = gangle * Math.PI / 180;\n let s;\n if (Math.abs(a) > (1.0e-10)) {\n s = Math.sin(a) / (1 - Math.cos(a));\n } else {\n // FIXME: This blows up if the angle is exactly 0!\n s = 2 / a;\n }\n for (let i = 0; i < tlist.numberOfItems; ++i) {\n const xform = tlist.getItem(i);\n if (xform.type === 4) {\n // extract old center through mystical arts\n const rm = xform.matrix;\n oldcenter.y = (s * rm.e + rm.f) / 2;\n oldcenter.x = (rm.e - s * rm.f) / 2;\n tlist.removeItem(i);\n break;\n }\n }\n }\n const N = tlist.numberOfItems;\n let tx = 0, ty = 0, operation = 0;\n\n let firstM;\n if (N) {\n firstM = tlist.getItem(0).matrix;\n }\n\n let oldStartTransform;\n // first, if it was a scale then the second-last transform will be it\n if (N >= 3 && tlist.getItem(N - 2).type === 3 &&\n tlist.getItem(N - 3).type === 2 && tlist.getItem(N - 1).type === 2) {\n operation = 3; // scale\n\n // if the children are unrotated, pass the scale down directly\n // otherwise pass the equivalent matrix() down directly\n const tm = tlist.getItem(N - 3).matrix,\n sm = tlist.getItem(N - 2).matrix,\n tmn = tlist.getItem(N - 1).matrix;\n\n const children = selected.childNodes;\n let c = children.length;\n while (c--) {\n const child = children.item(c);\n tx = 0;\n ty = 0;\n if (child.nodeType === 1) {\n const childTlist = getTransformList(child);\n\n // some children might not have a transform (, , etc)\n if (!childTlist) { continue; }\n\n const m = transformListToTransform(childTlist).matrix;\n\n // Convert a matrix to a scale if applicable\n // if (hasMatrixTransform(childTlist) && childTlist.numberOfItems == 1) {\n // if (m.b==0 && m.c==0 && m.e==0 && m.f==0) {\n // childTlist.removeItem(0);\n // const translateOrigin = svgroot.createSVGTransform(),\n // scale = svgroot.createSVGTransform(),\n // translateBack = svgroot.createSVGTransform();\n // translateOrigin.setTranslate(0, 0);\n // scale.setScale(m.a, m.d);\n // translateBack.setTranslate(0, 0);\n // childTlist.appendItem(translateBack);\n // childTlist.appendItem(scale);\n // childTlist.appendItem(translateOrigin);\n // }\n // }\n\n const angle = getRotationAngle(child);\n oldStartTransform = context_.getStartTransform();\n const childxforms = [];\n context_.setStartTransform(child.getAttribute('transform'));\n if (angle || hasMatrixTransform(childTlist)) {\n const e2t = svgroot.createSVGTransform();\n e2t.setMatrix(matrixMultiply(tm, sm, tmn, m));\n childTlist.clear();\n childTlist.appendItem(e2t);\n childxforms.push(e2t);\n // if not rotated or skewed, push the [T][S][-T] down to the child\n } else {\n // update the transform list with translate,scale,translate\n\n // slide the [T][S][-T] from the front to the back\n // [T][S][-T][M] = [M][T2][S2][-T2]\n\n // (only bringing [-T] to the right of [M])\n // [T][S][-T][M] = [T][S][M][-T2]\n // [-T2] = [M_inv][-T][M]\n const t2n = matrixMultiply(m.inverse(), tmn, m);\n // [T2] is always negative translation of [-T2]\n const t2 = svgroot.createSVGMatrix();\n t2.e = -t2n.e;\n t2.f = -t2n.f;\n\n // [T][S][-T][M] = [M][T2][S2][-T2]\n // [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv]\n const s2 = matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse());\n\n const translateOrigin = svgroot.createSVGTransform(),\n scale = svgroot.createSVGTransform(),\n translateBack = svgroot.createSVGTransform();\n translateOrigin.setTranslate(t2n.e, t2n.f);\n scale.setScale(s2.a, s2.d);\n translateBack.setTranslate(t2.e, t2.f);\n childTlist.appendItem(translateBack);\n childTlist.appendItem(scale);\n childTlist.appendItem(translateOrigin);\n childxforms.push(translateBack);\n childxforms.push(scale);\n childxforms.push(translateOrigin);\n // logMatrix(translateBack.matrix);\n // logMatrix(scale.matrix);\n } // not rotated\n batchCmd.addSubCommand(recalculateDimensions(child));\n // TODO: If any have this group as a parent and are\n // referencing this child, then we need to impose a reverse\n // scale on it so that when it won't get double-translated\n // const uses = selected.getElementsByTagNameNS(NS.SVG, 'use');\n // const href = '#' + child.id;\n // let u = uses.length;\n // while (u--) {\n // const useElem = uses.item(u);\n // if (href == getHref(useElem)) {\n // const usexlate = svgroot.createSVGTransform();\n // usexlate.setTranslate(-tx,-ty);\n // getTransformList(useElem).insertItemBefore(usexlate,0);\n // batchCmd.addSubCommand( recalculateDimensions(useElem) );\n // }\n // }\n context_.setStartTransform(oldStartTransform);\n } // element\n } // for each child\n // Remove these transforms from group\n tlist.removeItem(N - 1);\n tlist.removeItem(N - 2);\n tlist.removeItem(N - 3);\n } else if (N >= 3 && tlist.getItem(N - 1).type === 1) {\n operation = 3; // scale\n m = transformListToTransform(tlist).matrix;\n const e2t = svgroot.createSVGTransform();\n e2t.setMatrix(m);\n tlist.clear();\n tlist.appendItem(e2t);\n // next, check if the first transform was a translate\n // if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ]\n // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ]\n } else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&\n tlist.getItem(0).type === 2) {\n operation = 2; // translate\n const T_M = transformListToTransform(tlist).matrix;\n tlist.removeItem(0);\n const mInv = transformListToTransform(tlist).matrix.inverse();\n const M2 = matrixMultiply(mInv, T_M);\n\n tx = M2.e;\n ty = M2.f;\n\n if (tx !== 0 || ty !== 0) {\n // we pass the translates down to the individual children\n const children = selected.childNodes;\n let c = children.length;\n\n let clipPathsDone = [];\n\n while (c--) {\n const child = children.item(c);\n if (child.nodeType === 1) {\n // Check if child has clip-path\n if (child.getAttribute('clip-path')) {\n // tx, ty\n const attr = child.getAttribute('clip-path');\n if (!clipPathsDone.includes(attr)) {\n updateClipPath(attr, tx, ty);\n clipPathsDone.push(attr);\n }\n }\n\n oldStartTransform = context_.getStartTransform();\n context_.setStartTransform(child.getAttribute('transform'));\n\n const childTlist = getTransformList(child);\n // some children might not have a transform (, , etc)\n if (childTlist) {\n const newxlate = svgroot.createSVGTransform();\n newxlate.setTranslate(tx, ty);\n if (childTlist.numberOfItems) {\n childTlist.insertItemBefore(newxlate, 0);\n } else {\n childTlist.appendItem(newxlate);\n }\n batchCmd.addSubCommand(recalculateDimensions(child));\n // If any have this group as a parent and are\n // referencing this child, then impose a reverse translate on it\n // so that when it won't get double-translated\n const uses = selected.getElementsByTagNameNS(NS.SVG, 'use');\n const href = '#' + child.id;\n let u = uses.length;\n while (u--) {\n const useElem = uses.item(u);\n if (href === getHref(useElem)) {\n const usexlate = svgroot.createSVGTransform();\n usexlate.setTranslate(-tx, -ty);\n getTransformList(useElem).insertItemBefore(usexlate, 0);\n batchCmd.addSubCommand(recalculateDimensions(useElem));\n }\n }\n context_.setStartTransform(oldStartTransform);\n }\n }\n }\n\n clipPathsDone = [];\n context_.setStartTransform(oldStartTransform);\n }\n // else, a matrix imposition from a parent group\n // keep pushing it down to the children\n } else if (N === 1 && tlist.getItem(0).type === 1 && !gangle) {\n operation = 1;\n const m = tlist.getItem(0).matrix,\n children = selected.childNodes;\n let c = children.length;\n while (c--) {\n const child = children.item(c);\n if (child.nodeType === 1) {\n oldStartTransform = context_.getStartTransform();\n context_.setStartTransform(child.getAttribute('transform'));\n const childTlist = getTransformList(child);\n\n if (!childTlist) { continue; }\n\n const em = matrixMultiply(m, transformListToTransform(childTlist).matrix);\n const e2m = svgroot.createSVGTransform();\n e2m.setMatrix(em);\n childTlist.clear();\n childTlist.appendItem(e2m, 0);\n\n batchCmd.addSubCommand(recalculateDimensions(child));\n context_.setStartTransform(oldStartTransform);\n\n // Convert stroke\n // TODO: Find out if this should actually happen somewhere else\n const sw = child.getAttribute('stroke-width');\n if (child.getAttribute('stroke') !== 'none' && !isNaN(sw)) {\n const avg = (Math.abs(em.a) + Math.abs(em.d)) / 2;\n child.setAttribute('stroke-width', sw * avg);\n }\n }\n }\n tlist.clear();\n // else it was just a rotate\n } else {\n if (gangle) {\n const newRot = svgroot.createSVGTransform();\n newRot.setRotate(gangle, newcenter.x, newcenter.y);\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(newRot, 0);\n } else {\n tlist.appendItem(newRot);\n }\n }\n if (tlist.numberOfItems === 0) {\n selected.removeAttribute('transform');\n }\n return null;\n }\n\n // if it was a translate, put back the rotate at the new center\n if (operation === 2) {\n if (gangle) {\n newcenter = {\n x: oldcenter.x + firstM.e,\n y: oldcenter.y + firstM.f\n };\n\n const newRot = svgroot.createSVGTransform();\n newRot.setRotate(gangle, newcenter.x, newcenter.y);\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(newRot, 0);\n } else {\n tlist.appendItem(newRot);\n }\n }\n // if it was a resize\n } else if (operation === 3) {\n const m = transformListToTransform(tlist).matrix;\n const roldt = svgroot.createSVGTransform();\n roldt.setRotate(gangle, oldcenter.x, oldcenter.y);\n const rold = roldt.matrix;\n const rnew = svgroot.createSVGTransform();\n rnew.setRotate(gangle, newcenter.x, newcenter.y);\n const rnewInv = rnew.matrix.inverse(),\n mInv = m.inverse(),\n extrat = matrixMultiply(mInv, rnewInv, rold, m);\n\n tx = extrat.e;\n ty = extrat.f;\n\n if (tx !== 0 || ty !== 0) {\n // now push this transform down to the children\n // we pass the translates down to the individual children\n const children = selected.childNodes;\n let c = children.length;\n while (c--) {\n const child = children.item(c);\n if (child.nodeType === 1) {\n oldStartTransform = context_.getStartTransform();\n context_.setStartTransform(child.getAttribute('transform'));\n const childTlist = getTransformList(child);\n const newxlate = svgroot.createSVGTransform();\n newxlate.setTranslate(tx, ty);\n if (childTlist.numberOfItems) {\n childTlist.insertItemBefore(newxlate, 0);\n } else {\n childTlist.appendItem(newxlate);\n }\n\n batchCmd.addSubCommand(recalculateDimensions(child));\n context_.setStartTransform(oldStartTransform);\n }\n }\n }\n\n if (gangle) {\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(rnew, 0);\n } else {\n tlist.appendItem(rnew);\n }\n }\n }\n // else, it's a non-group\n } else {\n // FIXME: box might be null for some elements ( etc), need to handle this\n const box = getBBox(selected);\n\n // Paths (and possbly other shapes) will have no BBox while still in ,\n // but we still may need to recalculate them (see issue 595).\n // TODO: Figure out how to get BBox from these elements in case they\n // have a rotation transform\n\n if (!box && selected.tagName !== 'path') return null;\n\n let m = svgroot.createSVGMatrix();\n // temporarily strip off the rotate and save the old center\n const angle = getRotationAngle(selected);\n if (angle) {\n oldcenter = {x: box.x + box.width / 2, y: box.y + box.height / 2};\n newcenter = transformPoint(\n box.x + box.width / 2,\n box.y + box.height / 2,\n transformListToTransform(tlist).matrix\n );\n\n const a = angle * Math.PI / 180;\n const s = (Math.abs(a) > (1.0e-10))\n ? Math.sin(a) / (1 - Math.cos(a))\n // FIXME: This blows up if the angle is exactly 0!\n : 2 / a;\n\n for (let i = 0; i < tlist.numberOfItems; ++i) {\n const xform = tlist.getItem(i);\n if (xform.type === 4) {\n // extract old center through mystical arts\n const rm = xform.matrix;\n oldcenter.y = (s * rm.e + rm.f) / 2;\n oldcenter.x = (rm.e - s * rm.f) / 2;\n tlist.removeItem(i);\n break;\n }\n }\n }\n\n // 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition\n let operation = 0;\n const N = tlist.numberOfItems;\n\n // Check if it has a gradient with userSpaceOnUse, in which case\n // adjust it by recalculating the matrix transform.\n // TODO: Make this work in Webkit using svgedit.transformlist.SVGTransformList\n if (!isWebkit()) {\n const fill = selected.getAttribute('fill');\n if (fill && fill.startsWith('url(')) {\n const paint = getRefElem(fill);\n let type = 'pattern';\n if (paint.tagName !== type) type = 'gradient';\n const attrVal = paint.getAttribute(type + 'Units');\n if (attrVal === 'userSpaceOnUse') {\n // Update the userSpaceOnUse element\n m = transformListToTransform(tlist).matrix;\n const gtlist = getTransformList(paint);\n const gmatrix = transformListToTransform(gtlist).matrix;\n m = matrixMultiply(m, gmatrix);\n const mStr = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')';\n paint.setAttribute(type + 'Transform', mStr);\n }\n }\n }\n\n // first, if it was a scale of a non-skewed element, then the second-last\n // transform will be the [S]\n // if we had [M][T][S][T] we want to extract the matrix equivalent of\n // [T][S][T] and push it down to the element\n if (N >= 3 && tlist.getItem(N - 2).type === 3 &&\n tlist.getItem(N - 3).type === 2 && tlist.getItem(N - 1).type === 2) {\n // Removed this so a with a given [T][S][T] would convert to a matrix.\n // Is that bad?\n // && selected.nodeName != 'use'\n operation = 3; // scale\n m = transformListToTransform(tlist, N - 3, N - 1).matrix;\n tlist.removeItem(N - 1);\n tlist.removeItem(N - 2);\n tlist.removeItem(N - 3);\n // if we had [T][S][-T][M], then this was a skewed element being resized\n // Thus, we simply combine it all into one matrix\n } else if (N === 4 && tlist.getItem(N - 1).type === 1) {\n operation = 3; // scale\n m = transformListToTransform(tlist).matrix;\n const e2t = svgroot.createSVGTransform();\n e2t.setMatrix(m);\n tlist.clear();\n tlist.appendItem(e2t);\n // reset the matrix so that the element is not re-mapped\n m = svgroot.createSVGMatrix();\n // if we had [R][T][S][-T][M], then this was a rotated matrix-element\n // if we had [T1][M] we want to transform this into [M][T2]\n // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]\n // down to the element\n } else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&\n tlist.getItem(0).type === 2) {\n operation = 2; // translate\n const oldxlate = tlist.getItem(0).matrix,\n meq = transformListToTransform(tlist, 1).matrix,\n meqInv = meq.inverse();\n m = matrixMultiply(meqInv, oldxlate, meq);\n tlist.removeItem(0);\n // else if this child now has a matrix imposition (from a parent group)\n // we might be able to simplify\n } else if (N === 1 && tlist.getItem(0).type === 1 && !angle) {\n // Remap all point-based elements\n m = transformListToTransform(tlist).matrix;\n switch (selected.tagName) {\n case 'line':\n changes = $(selected).attr(['x1', 'y1', 'x2', 'y2']);\n // Fallthrough\n case 'polyline':\n case 'polygon':\n changes.points = selected.getAttribute('points');\n if (changes.points) {\n const list = selected.points;\n const len = list.numberOfItems;\n changes.points = new Array(len);\n for (let i = 0; i < len; ++i) {\n const pt = list.getItem(i);\n changes.points[i] = {x: pt.x, y: pt.y};\n }\n }\n // Fallthrough\n case 'path':\n changes.d = selected.getAttribute('d');\n operation = 1;\n tlist.clear();\n break;\n default:\n break;\n }\n // if it was a rotation, put the rotate back and return without a command\n // (this function has zero work to do for a rotate())\n } else {\n operation = 4; // rotation\n if (angle) {\n const newRot = svgroot.createSVGTransform();\n newRot.setRotate(angle, newcenter.x, newcenter.y);\n\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(newRot, 0);\n } else {\n tlist.appendItem(newRot);\n }\n }\n if (tlist.numberOfItems === 0) {\n selected.removeAttribute('transform');\n }\n return null;\n }\n\n // if it was a translate or resize, we need to remap the element and absorb the xform\n if (operation === 1 || operation === 2 || operation === 3) {\n remapElement(selected, changes, m);\n } // if we are remapping\n\n // if it was a translate, put back the rotate at the new center\n if (operation === 2) {\n if (angle) {\n if (!hasMatrixTransform(tlist)) {\n newcenter = {\n x: oldcenter.x + m.e,\n y: oldcenter.y + m.f\n };\n }\n const newRot = svgroot.createSVGTransform();\n newRot.setRotate(angle, newcenter.x, newcenter.y);\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(newRot, 0);\n } else {\n tlist.appendItem(newRot);\n }\n }\n // We have special processing for tspans: Tspans are not transformable\n // but they can have x,y coordinates (sigh). Thus, if this was a translate,\n // on a text element, also translate any tspan children.\n if (selected.tagName === 'text') {\n const children = selected.childNodes;\n let c = children.length;\n while (c--) {\n const child = children.item(c);\n if (child.tagName === 'tspan') {\n const tspanChanges = {\n x: $(child).attr('x') || 0,\n y: $(child).attr('y') || 0\n };\n remapElement(child, tspanChanges, m);\n }\n }\n }\n // [Rold][M][T][S][-T] became [Rold][M]\n // we want it to be [Rnew][M][Tr] where Tr is the\n // translation required to re-center it\n // Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M]\n } else if (operation === 3 && angle) {\n const m = transformListToTransform(tlist).matrix;\n const roldt = svgroot.createSVGTransform();\n roldt.setRotate(angle, oldcenter.x, oldcenter.y);\n const rold = roldt.matrix;\n const rnew = svgroot.createSVGTransform();\n rnew.setRotate(angle, newcenter.x, newcenter.y);\n const rnewInv = rnew.matrix.inverse();\n const mInv = m.inverse();\n const extrat = matrixMultiply(mInv, rnewInv, rold, m);\n\n remapElement(selected, changes, extrat);\n if (angle) {\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(rnew, 0);\n } else {\n tlist.appendItem(rnew);\n }\n }\n }\n } // a non-group\n\n // if the transform list has been emptied, remove it\n if (tlist.numberOfItems === 0) {\n selected.removeAttribute('transform');\n }\n\n batchCmd.addSubCommand(new ChangeElementCommand(selected, initial));\n\n return batchCmd;\n};\n","/* globals jQuery */\n/**\n * Package: svedit.select\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Alexis Deveria\n * Copyright(c) 2010 Jeff Schiller\n */\n\nimport {isTouch, isWebkit} from './browser.js'; // , isOpera\nimport {getRotationAngle, getBBox, getStrokedBBox} from './svgutils.js';\nimport {transformListToTransform, transformBox, transformPoint} from './math.js';\nimport {getTransformList} from './svgtransformlist.js';\n\nconst $ = jQuery;\n\nlet svgFactory_;\nlet config_;\nlet selectorManager_; // A Singleton\nconst gripRadius = isTouch() ? 10 : 4;\n\n/**\n* Private class for DOM element selection boxes\n* @param id - integer to internally indentify the selector\n* @param elem - DOM element associated with this selector\n* @param bbox - Optional bbox to use for initialization (prevents duplicate getBBox call).\n*/\nexport class Selector {\n constructor (id, elem, bbox) {\n // this is the selector's unique number\n this.id = id;\n\n // this holds a reference to the element for which this selector is being used\n this.selectedElement = elem;\n\n // this is a flag used internally to track whether the selector is being used or not\n this.locked = true;\n\n // this holds a reference to the element that holds all visual elements of the selector\n this.selectorGroup = svgFactory_.createSVGElement({\n element: 'g',\n attr: {id: ('selectorGroup' + this.id)}\n });\n\n // this holds a reference to the path rect\n this.selectorRect = this.selectorGroup.appendChild(\n svgFactory_.createSVGElement({\n element: 'path',\n attr: {\n id: ('selectedBox' + this.id),\n fill: 'none',\n stroke: '#22C',\n 'stroke-width': '1',\n 'stroke-dasharray': '5,5',\n // need to specify this so that the rect is not selectable\n style: 'pointer-events:none'\n }\n })\n );\n\n // this holds a reference to the grip coordinates for this selector\n this.gripCoords = {\n nw: null,\n n: null,\n ne: null,\n e: null,\n se: null,\n s: null,\n sw: null,\n w: null\n };\n\n this.reset(this.selectedElement, bbox);\n }\n\n /**\n * Used to reset the id and element that the selector is attached to\n * @param e - DOM element associated with this selector\n * @param bbox - Optional bbox to use for reset (prevents duplicate getBBox call).\n */\n reset (e, bbox) {\n this.locked = true;\n this.selectedElement = e;\n this.resize(bbox);\n this.selectorGroup.setAttribute('display', 'inline');\n }\n\n /**\n * Updates cursors for corner grips on rotation so arrows point the right way\n * @param {Number} angle - Float indicating current rotation angle in degrees\n */\n updateGripCursors (angle) {\n let dir;\n const dirArr = [];\n let steps = Math.round(angle / 45);\n if (steps < 0) { steps += 8; }\n for (dir in selectorManager_.selectorGrips) {\n dirArr.push(dir);\n }\n while (steps > 0) {\n dirArr.push(dirArr.shift());\n steps--;\n }\n let i = 0;\n for (dir in selectorManager_.selectorGrips) {\n selectorManager_.selectorGrips[dir].setAttribute('style', ('cursor:' + dirArr[i] + '-resize'));\n i++;\n }\n }\n\n /**\n * Show the resize grips of this selector\n *\n * @param {Boolean} show - Indicates whether grips should be shown or not\n */\n showGrips (show) {\n const bShow = show ? 'inline' : 'none';\n selectorManager_.selectorGripsGroup.setAttribute('display', bShow);\n const elem = this.selectedElement;\n this.hasGrips = show;\n if (elem && show) {\n this.selectorGroup.appendChild(selectorManager_.selectorGripsGroup);\n this.updateGripCursors(getRotationAngle(elem));\n }\n }\n\n /**\n * Updates the selector to match the element's size\n * @param bbox - Optional bbox to use for resize (prevents duplicate getBBox call).\n */\n resize (bbox) {\n const selectedBox = this.selectorRect,\n mgr = selectorManager_,\n selectedGrips = mgr.selectorGrips,\n selected = this.selectedElement,\n sw = selected.getAttribute('stroke-width'),\n currentZoom = svgFactory_.getCurrentZoom();\n let offset = 1 / currentZoom;\n if (selected.getAttribute('stroke') !== 'none' && !isNaN(sw)) {\n offset += (sw / 2);\n }\n\n const {tagName} = selected;\n if (tagName === 'text') {\n offset += 2 / currentZoom;\n }\n\n // loop and transform our bounding box until we reach our first rotation\n const tlist = getTransformList(selected);\n const m = transformListToTransform(tlist).matrix;\n\n // This should probably be handled somewhere else, but for now\n // it keeps the selection box correctly positioned when zoomed\n m.e *= currentZoom;\n m.f *= currentZoom;\n\n if (!bbox) {\n bbox = getBBox(selected);\n }\n // TODO: getBBox (previous line) already knows to call getStrokedBBox when tagName === 'g'. Remove this?\n // TODO: getBBox doesn't exclude 'gsvg' and calls getStrokedBBox for any 'g'. Should getBBox be updated?\n if (tagName === 'g' && !$.data(selected, 'gsvg')) {\n // The bbox for a group does not include stroke vals, so we\n // get the bbox based on its children.\n const strokedBbox = getStrokedBBox([selected.childNodes]);\n if (strokedBbox) {\n bbox = strokedBbox;\n }\n }\n\n // apply the transforms\n const l = bbox.x, t = bbox.y, w = bbox.width, h = bbox.height;\n bbox = {x: l, y: t, width: w, height: h};\n\n // we need to handle temporary transforms too\n // if skewed, get its transformed box, then find its axis-aligned bbox\n\n // *\n offset *= currentZoom;\n\n const nbox = transformBox(l * currentZoom, t * currentZoom, w * currentZoom, h * currentZoom, m),\n {aabox} = nbox;\n let nbax = aabox.x - offset,\n nbay = aabox.y - offset,\n nbaw = aabox.width + (offset * 2),\n nbah = aabox.height + (offset * 2);\n\n // now if the shape is rotated, un-rotate it\n const cx = nbax + nbaw / 2,\n cy = nbay + nbah / 2;\n\n const angle = getRotationAngle(selected);\n if (angle) {\n const rot = svgFactory_.svgRoot().createSVGTransform();\n rot.setRotate(-angle, cx, cy);\n const rotm = rot.matrix;\n nbox.tl = transformPoint(nbox.tl.x, nbox.tl.y, rotm);\n nbox.tr = transformPoint(nbox.tr.x, nbox.tr.y, rotm);\n nbox.bl = transformPoint(nbox.bl.x, nbox.bl.y, rotm);\n nbox.br = transformPoint(nbox.br.x, nbox.br.y, rotm);\n\n // calculate the axis-aligned bbox\n const {tl} = nbox;\n let minx = tl.x,\n miny = tl.y,\n maxx = tl.x,\n maxy = tl.y;\n\n const {min, max} = Math;\n\n minx = min(minx, min(nbox.tr.x, min(nbox.bl.x, nbox.br.x))) - offset;\n miny = min(miny, min(nbox.tr.y, min(nbox.bl.y, nbox.br.y))) - offset;\n maxx = max(maxx, max(nbox.tr.x, max(nbox.bl.x, nbox.br.x))) + offset;\n maxy = max(maxy, max(nbox.tr.y, max(nbox.bl.y, nbox.br.y))) + offset;\n\n nbax = minx;\n nbay = miny;\n nbaw = (maxx - minx);\n nbah = (maxy - miny);\n }\n\n const dstr = 'M' + nbax + ',' + nbay +\n ' L' + (nbax + nbaw) + ',' + nbay +\n ' ' + (nbax + nbaw) + ',' + (nbay + nbah) +\n ' ' + nbax + ',' + (nbay + nbah) + 'z';\n selectedBox.setAttribute('d', dstr);\n\n const xform = angle ? 'rotate(' + [angle, cx, cy].join(',') + ')' : '';\n this.selectorGroup.setAttribute('transform', xform);\n\n // TODO(codedread): Is this needed?\n // if (selected === selectedElements[0]) {\n this.gripCoords = {\n nw: [nbax, nbay],\n ne: [nbax + nbaw, nbay],\n sw: [nbax, nbay + nbah],\n se: [nbax + nbaw, nbay + nbah],\n n: [nbax + (nbaw) / 2, nbay],\n w: [nbax, nbay + (nbah) / 2],\n e: [nbax + nbaw, nbay + (nbah) / 2],\n s: [nbax + (nbaw) / 2, nbay + nbah]\n };\n for (const dir in this.gripCoords) {\n const coords = this.gripCoords[dir];\n selectedGrips[dir].setAttribute('cx', coords[0]);\n selectedGrips[dir].setAttribute('cy', coords[1]);\n }\n\n // we want to go 20 pixels in the negative transformed y direction, ignoring scale\n mgr.rotateGripConnector.setAttribute('x1', nbax + (nbaw) / 2);\n mgr.rotateGripConnector.setAttribute('y1', nbay);\n mgr.rotateGripConnector.setAttribute('x2', nbax + (nbaw) / 2);\n mgr.rotateGripConnector.setAttribute('y2', nbay - (gripRadius * 5));\n\n mgr.rotateGrip.setAttribute('cx', nbax + (nbaw) / 2);\n mgr.rotateGrip.setAttribute('cy', nbay - (gripRadius * 5));\n // }\n }\n}\n\n/**\n*\n*/\nexport class SelectorManager {\n constructor () {\n // this will hold the element that contains all selector rects/grips\n this.selectorParentGroup = null;\n\n // this is a special rect that is used for multi-select\n this.rubberBandBox = null;\n\n // this will hold objects of type Selector (see above)\n this.selectors = [];\n\n // this holds a map of SVG elements to their Selector object\n this.selectorMap = {};\n\n // this holds a reference to the grip elements\n this.selectorGrips = {\n nw: null,\n n: null,\n ne: null,\n e: null,\n se: null,\n s: null,\n sw: null,\n w: null\n };\n\n this.selectorGripsGroup = null;\n this.rotateGripConnector = null;\n this.rotateGrip = null;\n\n this.initGroup();\n }\n\n /**\n * Resets the parent selector group element\n */\n initGroup () {\n // remove old selector parent group if it existed\n if (this.selectorParentGroup && this.selectorParentGroup.parentNode) {\n this.selectorParentGroup.parentNode.removeChild(this.selectorParentGroup);\n }\n\n // create parent selector group and add it to svgroot\n this.selectorParentGroup = svgFactory_.createSVGElement({\n element: 'g',\n attr: {id: 'selectorParentGroup'}\n });\n this.selectorGripsGroup = svgFactory_.createSVGElement({\n element: 'g',\n attr: {display: 'none'}\n });\n this.selectorParentGroup.appendChild(this.selectorGripsGroup);\n svgFactory_.svgRoot().appendChild(this.selectorParentGroup);\n\n this.selectorMap = {};\n this.selectors = [];\n this.rubberBandBox = null;\n\n // add the corner grips\n for (const dir in this.selectorGrips) {\n const grip = svgFactory_.createSVGElement({\n element: 'circle',\n attr: {\n id: ('selectorGrip_resize_' + dir),\n fill: '#22C',\n r: gripRadius,\n style: ('cursor:' + dir + '-resize'),\n // This expands the mouse-able area of the grips making them\n // easier to grab with the mouse.\n // This works in Opera and WebKit, but does not work in Firefox\n // see https://bugzilla.mozilla.org/show_bug.cgi?id=500174\n 'stroke-width': 2,\n 'pointer-events': 'all'\n }\n });\n\n $.data(grip, 'dir', dir);\n $.data(grip, 'type', 'resize');\n this.selectorGrips[dir] = this.selectorGripsGroup.appendChild(grip);\n }\n\n // add rotator elems\n this.rotateGripConnector = this.selectorGripsGroup.appendChild(\n svgFactory_.createSVGElement({\n element: 'line',\n attr: {\n id: ('selectorGrip_rotateconnector'),\n stroke: '#22C',\n 'stroke-width': '1'\n }\n })\n );\n\n this.rotateGrip = this.selectorGripsGroup.appendChild(\n svgFactory_.createSVGElement({\n element: 'circle',\n attr: {\n id: 'selectorGrip_rotate',\n fill: 'lime',\n r: gripRadius,\n stroke: '#22C',\n 'stroke-width': 2,\n style: 'cursor:url(' + config_.imgPath + 'rotate.png) 12 12, auto;'\n }\n })\n );\n $.data(this.rotateGrip, 'type', 'rotate');\n\n if ($('#canvasBackground').length) { return; }\n\n const dims = config_.dimensions;\n const canvasbg = svgFactory_.createSVGElement({\n element: 'svg',\n attr: {\n id: 'canvasBackground',\n width: dims[0],\n height: dims[1],\n x: 0,\n y: 0,\n overflow: (isWebkit() ? 'none' : 'visible'), // Chrome 7 has a problem with this when zooming out\n style: 'pointer-events:none'\n }\n });\n\n const rect = svgFactory_.createSVGElement({\n element: 'rect',\n attr: {\n width: '100%',\n height: '100%',\n x: 0,\n y: 0,\n 'stroke-width': 1,\n stroke: '#000',\n fill: '#FFF',\n style: 'pointer-events:none'\n }\n });\n\n // Both Firefox and WebKit are too slow with this filter region (especially at higher\n // zoom levels) and Opera has at least one bug\n // if (!isOpera()) rect.setAttribute('filter', 'url(#canvashadow)');\n canvasbg.appendChild(rect);\n svgFactory_.svgRoot().insertBefore(canvasbg, svgFactory_.svgContent());\n }\n\n /**\n *\n * @param elem - DOM element to get the selector for\n * @param [bbox] - Optional bbox to use for reset (prevents duplicate getBBox call).\n * @returns The selector based on the given element\n */\n requestSelector (elem, bbox) {\n if (elem == null) { return null; }\n\n const N = this.selectors.length;\n // If we've already acquired one for this element, return it.\n if (typeof this.selectorMap[elem.id] === 'object') {\n this.selectorMap[elem.id].locked = true;\n return this.selectorMap[elem.id];\n }\n for (let i = 0; i < N; ++i) {\n if (this.selectors[i] && !this.selectors[i].locked) {\n this.selectors[i].locked = true;\n this.selectors[i].reset(elem, bbox);\n this.selectorMap[elem.id] = this.selectors[i];\n return this.selectors[i];\n }\n }\n // if we reached here, no available selectors were found, we create one\n this.selectors[N] = new Selector(N, elem, bbox);\n this.selectorParentGroup.appendChild(this.selectors[N].selectorGroup);\n this.selectorMap[elem.id] = this.selectors[N];\n return this.selectors[N];\n }\n\n /**\n * Removes the selector of the given element (hides selection box)\n *\n * @param elem - DOM element to remove the selector for\n */\n releaseSelector (elem) {\n if (elem == null) { return; }\n const N = this.selectors.length,\n sel = this.selectorMap[elem.id];\n if (!sel.locked) {\n // TODO(codedread): Ensure this exists in this module.\n console.log('WARNING! selector was released but was already unlocked');\n }\n for (let i = 0; i < N; ++i) {\n if (this.selectors[i] && this.selectors[i] === sel) {\n delete this.selectorMap[elem.id];\n sel.locked = false;\n sel.selectedElement = null;\n sel.showGrips(false);\n\n // remove from DOM and store reference in JS but only if it exists in the DOM\n try {\n sel.selectorGroup.setAttribute('display', 'none');\n } catch (e) {}\n\n break;\n }\n }\n }\n\n /**\n * @returns The rubberBandBox DOM element. This is the rectangle drawn by\n * the user for selecting/zooming\n */\n getRubberBandBox () {\n if (!this.rubberBandBox) {\n this.rubberBandBox = this.selectorParentGroup.appendChild(\n svgFactory_.createSVGElement({\n element: 'rect',\n attr: {\n id: 'selectorRubberBand',\n fill: '#22C',\n 'fill-opacity': 0.15,\n stroke: '#22C',\n 'stroke-width': 0.5,\n display: 'none',\n style: 'pointer-events:none'\n }\n })\n );\n }\n return this.rubberBandBox;\n }\n}\n\n/**\n * An object that creates SVG elements for the canvas.\n *\n * interface svgedit.select.SVGFactory {\n * SVGElement createSVGElement(jsonMap);\n * SVGSVGElement svgRoot();\n * SVGSVGElement svgContent();\n *\n * Number currentZoom();\n * }\n */\n\n/**\n * Initializes this module.\n *\n * @param config - An object containing configurable parameters (imgPath)\n * @param svgFactory - An object implementing the SVGFactory interface (see above).\n */\nexport const init = function (config, svgFactory) {\n config_ = config;\n svgFactory_ = svgFactory;\n selectorManager_ = new SelectorManager();\n};\n\n/**\n *\n * @returns The SelectorManager instance.\n */\nexport const getSelectorManager = () => selectorManager_;\n","/* eslint-disable indent */\n/* globals jQuery, jsPDF */\n/*\n * svgcanvas.js\n *\n * Licensed under the MIT License\n *\n * Copyright(c) 2010 Alexis Deveria\n * Copyright(c) 2010 Pavol Rusnak\n * Copyright(c) 2010 Jeff Schiller\n *\n */\n\n/* Dependencies:\n1. Also expects jQuery UI for `svgCanvasToString` and\n`convertToGroup` use of `:data()` selector\n*/\n\nimport './pathseg.js';\nimport canvg from './canvg/canvg.js';\nimport jqPluginSVG from './jquery-svg.js'; // Needed for SVG attribute setting and array form with `attr`\n\nimport * as draw from './draw.js';\nimport * as pathModule from './path.js';\nimport {sanitizeSvg} from './sanitize.js';\nimport {getReverseNS, NS} from './svgedit.js';\nimport {\n text2xml, assignAttributes, cleanupElement, getElem, getUrlFromAttr,\n findDefs, getHref, setHref, getRefElem, getRotationAngle, getPathBBox,\n preventClickDefault, snapToGrid, walkTree, walkTreePost,\n getBBoxOfElementAsPath, convertToPath, toXml, encode64, decode64,\n buildJSPDFCallback, dataURLToObjectURL, createObjectURL,\n buildCanvgCallback, getVisibleElements, executeAfterLoads,\n init as utilsInit, getBBox as utilsGetBBox, getStrokedBBoxDefaultVisible\n} from './svgutils.js';\nimport * as history from './history.js';\nimport {\n transformPoint, matrixMultiply, hasMatrixTransform, transformListToTransform,\n getMatrix, snapToAngle, isIdentity, rectsIntersect, transformBox\n} from './math.js';\nimport {\n convertToNum, convertAttrs, convertUnit, shortFloat, getTypeMap,\n init as unitsInit\n} from './units.js';\nimport {\n isGecko, isChrome, isIE, isWebkit, supportsNonScalingStroke, supportsGoodTextCharPos\n} from './browser.js'; // , supportsEditableText\nimport {\n getTransformList, resetListMap,\n SVGTransformList as SVGEditTransformList\n} from './svgtransformlist.js';\nimport {\n remapElement,\n init as coordsInit\n} from './coords.js';\nimport {\n recalculateDimensions,\n init as recalculateInit\n} from './recalculate.js';\nimport {\n getSelectorManager,\n init as selectInit\n} from './select.js';\n\nconst $ = jqPluginSVG(jQuery);\nconst {\n MoveElementCommand, InsertElementCommand, RemoveElementCommand,\n ChangeElementCommand, BatchCommand, UndoManager, HistoryEventTypes\n} = history;\n\nif (!window.console) {\n window.console = {};\n window.console.log = function (str) {};\n window.console.dir = function (str) {};\n}\n\nif (window.opera) {\n window.console.log = function (str) { opera.postError(str); };\n window.console.dir = function (str) {};\n}\n\n/**\n* The main SvgCanvas class that manages all SVG-related functions\n* @param container - The container HTML element that should hold the SVG root element\n* @param {Object} config - An object that contains configuration data\n*/\nexport default class {\n constructor (container, config) {\n// Alias Namespace constants\n\n// Default configuration options\nconst curConfig = {\n show_outside_canvas: true,\n selectNew: true,\n dimensions: [640, 480]\n};\n\n// Update config with new one if given\nif (config) {\n $.extend(curConfig, config);\n}\n\n// Array with width/height of canvas\nconst {dimensions} = curConfig;\n\nconst canvas = this;\n\n// \"document\" element associated with the container (same as window.document using default svg-editor.js)\n// NOTE: This is not actually a SVG document, but an HTML document.\nconst svgdoc = container.ownerDocument;\n\n// This is a container for the document being edited, not the document itself.\nconst svgroot = svgdoc.importNode(\n text2xml(\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n ''\n ).documentElement,\n true\n);\ncontainer.appendChild(svgroot);\n\n// The actual element that represents the final output SVG element\nlet svgcontent = svgdoc.createElementNS(NS.SVG, 'svg');\n\n// This function resets the svgcontent element while keeping it in the DOM.\nconst clearSvgContentElement = canvas.clearSvgContentElement = function () {\n $(svgcontent).empty();\n\n // TODO: Clear out all other attributes first?\n $(svgcontent).attr({\n id: 'svgcontent',\n width: dimensions[0],\n height: dimensions[1],\n x: dimensions[0],\n y: dimensions[1],\n overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden',\n xmlns: NS.SVG,\n 'xmlns:se': NS.SE,\n 'xmlns:xlink': NS.XLINK\n }).appendTo(svgroot);\n\n // TODO: make this string optional and set by the client\n const comment = svgdoc.createComment(' Created with SVG-edit - https://github.com/SVG-Edit/svgedit');\n svgcontent.appendChild(comment);\n};\nclearSvgContentElement();\n\n// Prefix string for element IDs\nlet idprefix = 'svg_';\n\n/**\n* Changes the ID prefix to the given value\n* @param {String} p - String with the new prefix\n*/\ncanvas.setIdPrefix = function (p) {\n idprefix = p;\n};\n\n// Current svgedit.draw.Drawing object\n// @type {svgedit.draw.Drawing}\ncanvas.current_drawing_ = new draw.Drawing(svgcontent, idprefix);\n\n/**\n* Returns the current Drawing.\n* @returns {svgedit.draw.Drawing}\n*/\nconst getCurrentDrawing = canvas.getCurrentDrawing = function () {\n return canvas.current_drawing_;\n};\n\n// Float displaying the current zoom level (1 = 100%, .5 = 50%, etc)\nlet currentZoom = 1;\n\n// pointer to current group (for in-group editing)\nlet currentGroup = null;\n\n// Object containing data for the currently selected styles\nconst allProperties = {\n shape: {\n fill: (curConfig.initFill.color === 'none' ? '' : '#') + curConfig.initFill.color,\n fill_paint: null,\n fill_opacity: curConfig.initFill.opacity,\n stroke: '#' + curConfig.initStroke.color,\n stroke_paint: null,\n stroke_opacity: curConfig.initStroke.opacity,\n stroke_width: curConfig.initStroke.width,\n stroke_dasharray: 'none',\n stroke_linejoin: 'miter',\n stroke_linecap: 'butt',\n opacity: curConfig.initOpacity\n }\n};\n\nallProperties.text = $.extend(true, {}, allProperties.shape);\n$.extend(allProperties.text, {\n fill: '#000000',\n stroke_width: curConfig.text && curConfig.text.stroke_width,\n font_size: curConfig.text && curConfig.text.font_size,\n font_family: curConfig.text && curConfig.text.font_family\n});\n\n// Current shape style properties\nconst curShape = allProperties.shape;\n\n// Array with all the currently selected elements\n// default size of 1 until it needs to grow bigger\nlet selectedElements = [];\n\nconst getJsonFromSvgElement = this.getJsonFromSvgElement = function (data) {\n // Text node\n if (data.nodeType === 3) return data.nodeValue;\n\n const retval = {\n element: data.tagName,\n // namespace: nsMap[data.namespaceURI],\n attr: {},\n children: []\n };\n\n // Iterate attributes\n for (let i = 0, attr; (attr = data.attributes[i]); i++) {\n retval.attr[attr.name] = attr.value;\n }\n\n // Iterate children\n for (let i = 0, node; (node = data.childNodes[i]); i++) {\n retval.children[i] = getJsonFromSvgElement(node);\n }\n\n return retval;\n};\n\n/**\n* Create a new SVG element based on the given object keys/values and add it to the current layer\n* The element will be ran through cleanupElement before being returned\n*\n* @param data - Object with the following keys/values:\n* @param {String} data.element - tag name of the SVG element to create\n* @param {Object} data.attr - Has key-value attributes to assign to the new element\n* @param {Boolean} [data.curStyles] - Indicates whether current style attributes should be applied first\n* @param {Array} [data.children] - Data objects to be added recursively as children\n* @param {String} [data.namespace=\"http://www.w3.org/2000/svg\"] - Indicate a (non-SVG) namespace\n*\n* @returns The new element\n*/\nconst addSvgElementFromJson = this.addSvgElementFromJson = function (data) {\n if (typeof data === 'string') return svgdoc.createTextNode(data);\n\n let shape = getElem(data.attr.id);\n // if shape is a path but we need to create a rect/ellipse, then remove the path\n const currentLayer = getCurrentDrawing().getCurrentLayer();\n if (shape && data.element !== shape.tagName) {\n currentLayer.removeChild(shape);\n shape = null;\n }\n if (!shape) {\n const ns = data.namespace || NS.SVG;\n shape = svgdoc.createElementNS(ns, data.element);\n if (currentLayer) {\n (currentGroup || currentLayer).appendChild(shape);\n }\n }\n if (data.curStyles) {\n assignAttributes(shape, {\n fill: curShape.fill,\n stroke: curShape.stroke,\n 'stroke-width': curShape.stroke_width,\n 'stroke-dasharray': curShape.stroke_dasharray,\n 'stroke-linejoin': curShape.stroke_linejoin,\n 'stroke-linecap': curShape.stroke_linecap,\n 'stroke-opacity': curShape.stroke_opacity,\n 'fill-opacity': curShape.fill_opacity,\n opacity: curShape.opacity / 2,\n style: 'pointer-events:inherit'\n }, 100);\n }\n assignAttributes(shape, data.attr, 100);\n cleanupElement(shape);\n\n // Children\n if (data.children) {\n data.children.forEach(function (child) {\n shape.appendChild(addSvgElementFromJson(child));\n });\n }\n\n return shape;\n};\n\ncanvas.getTransformList = getTransformList;\n\ncanvas.matrixMultiply = matrixMultiply;\ncanvas.hasMatrixTransform = hasMatrixTransform;\ncanvas.transformListToTransform = transformListToTransform;\n\n// initialize from units.js\n// send in an object implementing the ElementContainer interface (see units.js)\nunitsInit({\n getBaseUnit () { return curConfig.baseUnit; },\n getElement: getElem,\n getHeight () { return svgcontent.getAttribute('height') / currentZoom; },\n getWidth () { return svgcontent.getAttribute('width') / currentZoom; },\n getRoundDigits () { return saveOptions.round_digits; }\n});\n\ncanvas.convertToNum = convertToNum;\n\nconst getSVGContent = () => { return svgcontent; };\n\n/**\n* @returns {Array} the array with selected DOM elements\n*/\nconst getSelectedElements = this.getSelectedElems = function () {\n return selectedElements;\n};\n\nconst pathActions = pathModule.pathActions;\n\nutilsInit({\n pathActions, // Ok since not modifying\n getSVGContent,\n addSvgElementFromJson,\n getSelectedElements,\n getDOMDocument () { return svgdoc; },\n getDOMContainer () { return container; },\n getSVGRoot () { return svgroot; },\n // TODO: replace this mostly with a way to get the current drawing.\n getBaseUnit () { return curConfig.baseUnit; },\n getSnappingStep () { return curConfig.snappingStep; }\n});\n\ncanvas.findDefs = findDefs;\ncanvas.getUrlFromAttr = getUrlFromAttr;\ncanvas.getHref = getHref;\ncanvas.setHref = setHref;\n/* const getBBox = */ canvas.getBBox = utilsGetBBox;\ncanvas.getRotationAngle = getRotationAngle;\ncanvas.getElem = getElem;\ncanvas.getRefElem = getRefElem;\ncanvas.assignAttributes = assignAttributes;\nthis.cleanupElement = cleanupElement;\n\nconst getGridSnapping = () => { return curConfig.gridSnapping; };\ncoordsInit({\n getDrawing () { return getCurrentDrawing(); },\n getSVGRoot () { return svgroot; },\n getGridSnapping\n});\nthis.remapElement = remapElement;\n\nrecalculateInit({\n getSVGRoot () { return svgroot; },\n getStartTransform () { return startTransform; },\n setStartTransform (transform) { startTransform = transform; }\n});\nthis.recalculateDimensions = recalculateDimensions;\n\n// import from sanitize.js\nconst nsMap = getReverseNS();\ncanvas.sanitizeSvg = sanitizeSvg;\n\n// Implement the svgedit.history.HistoryEventHandler interface.\nconst undoMgr = canvas.undoMgr = new UndoManager({\n handleHistoryEvent (eventType, cmd) {\n const EventTypes = HistoryEventTypes;\n // TODO: handle setBlurOffsets.\n if (eventType === EventTypes.BEFORE_UNAPPLY || eventType === EventTypes.BEFORE_APPLY) {\n canvas.clearSelection();\n } else if (eventType === EventTypes.AFTER_APPLY || eventType === EventTypes.AFTER_UNAPPLY) {\n const elems = cmd.elements();\n canvas.pathActions.clear();\n call('changed', elems);\n const cmdType = cmd.type();\n const isApply = (eventType === EventTypes.AFTER_APPLY);\n if (cmdType === MoveElementCommand.type()) {\n const parent = isApply ? cmd.newParent : cmd.oldParent;\n if (parent === svgcontent) {\n draw.identifyLayers();\n }\n } else if (cmdType === InsertElementCommand.type() ||\n cmdType === RemoveElementCommand.type()) {\n if (cmd.parent === svgcontent) {\n draw.identifyLayers();\n }\n if (cmdType === InsertElementCommand.type()) {\n if (isApply) { restoreRefElems(cmd.elem); }\n } else {\n if (!isApply) { restoreRefElems(cmd.elem); }\n }\n if (cmd.elem.tagName === 'use') {\n setUseData(cmd.elem);\n }\n } else if (cmdType === ChangeElementCommand.type()) {\n // if we are changing layer names, re-identify all layers\n if (cmd.elem.tagName === 'title' &&\n cmd.elem.parentNode.parentNode === svgcontent\n ) {\n draw.identifyLayers();\n }\n const values = isApply ? cmd.newValues : cmd.oldValues;\n // If stdDeviation was changed, update the blur.\n if (values.stdDeviation) {\n canvas.setBlurOffsets(cmd.elem.parentNode, values.stdDeviation);\n }\n // This is resolved in later versions of webkit, perhaps we should\n // have a featured detection for correct 'use' behavior?\n // ——————————\n // Remove & Re-add hack for Webkit (issue 775)\n // if (cmd.elem.tagName === 'use' && isWebkit()) {\n // const {elem} = cmd;\n // if (!elem.getAttribute('x') && !elem.getAttribute('y')) {\n // const parent = elem.parentNode;\n // const sib = elem.nextSibling;\n // parent.removeChild(elem);\n // parent.insertBefore(elem, sib);\n // }\n // }\n }\n }\n }\n});\nconst addCommandToHistory = function (cmd) {\n canvas.undoMgr.addCommandToHistory(cmd);\n};\n\n/**\n* @returns The current zoom level\n*/\nconst getCurrentZoom = this.getZoom = function () { return currentZoom; };\n\n// This method rounds the incoming value to the nearest value based on the currentZoom\nconst round = this.round = function (val) {\n return parseInt(val * currentZoom, 10) / currentZoom;\n};\n\n// import from select.js\nselectInit(curConfig, {\n createSVGElement (jsonMap) { return canvas.addSvgElementFromJson(jsonMap); },\n svgRoot () { return svgroot; },\n svgContent () { return svgcontent; },\n getCurrentZoom\n});\n// this object manages selectors for us\nconst selectorManager = this.selectorManager = getSelectorManager();\n\nconst getNextId = canvas.getNextId = function () { return getCurrentDrawing().getNextId(); };\nconst getId = canvas.getId = function () { return getCurrentDrawing().getId(); };\n\n/**\n* Run the callback function associated with the given event\n* @param ev - String with the event name\n* @param arg - Argument to pass through to the callback function\n*/\nconst call = function (ev, arg) {\n if (events[ev]) {\n return events[ev](window, arg);\n }\n};\n\n/**\n* Clears the selection. The 'selected' handler is then called.\n* @param {Boolean} [noCall] - When true does not call the \"selected\" handler\n*/\nconst clearSelection = function (noCall) {\n selectedElements.map(function (elem) {\n if (elem == null) return;\n\n selectorManager.releaseSelector(elem);\n });\n selectedElements = [];\n\n if (!noCall) { call('selected', selectedElements); }\n};\n\n/**\n* Adds a list of elements to the selection. The 'selected' handler is then called.\n* @param {Array} elemsToAdd - An array of DOM elements to add to the selection\n* @param {Boolean} showGrips - Indicates whether the resize grips should be shown\n*/\nconst addToSelection = function (elemsToAdd, showGrips) {\n if (!elemsToAdd.length) { return; }\n // find the first null in our selectedElements array\n\n let j = 0;\n while (j < selectedElements.length) {\n if (selectedElements[j] == null) {\n break;\n }\n ++j;\n }\n\n // now add each element consecutively\n let i = elemsToAdd.length;\n while (i--) {\n let elem = elemsToAdd[i];\n if (!elem) { continue; }\n const bbox = utilsGetBBox(elem);\n if (!bbox) { continue; }\n\n if (elem.tagName === 'a' && elem.childNodes.length === 1) {\n // Make \"a\" element's child be the selected element\n elem = elem.firstChild;\n }\n\n // if it's not already there, add it\n if (!selectedElements.includes(elem)) {\n selectedElements[j] = elem;\n\n // only the first selectedBBoxes element is ever used in the codebase these days\n // if (j === 0) selectedBBoxes[0] = utilsGetBBox(elem);\n j++;\n const sel = selectorManager.requestSelector(elem, bbox);\n\n if (selectedElements.length > 1) {\n sel.showGrips(false);\n }\n }\n }\n call('selected', selectedElements);\n\n if (showGrips || selectedElements.length === 1) {\n selectorManager.requestSelector(selectedElements[0]).showGrips(true);\n } else {\n selectorManager.requestSelector(selectedElements[0]).showGrips(false);\n }\n\n // make sure the elements are in the correct order\n // See: https://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition\n\n selectedElements.sort(function (a, b) {\n if (a && b && a.compareDocumentPosition) {\n return 3 - (b.compareDocumentPosition(a) & 6);\n }\n if (a == null) {\n return 1;\n }\n });\n\n // Make sure first elements are not null\n while (selectedElements[0] == null) {\n selectedElements.shift(0);\n }\n};\n\nconst getOpacity = function () {\n return curShape.opacity;\n};\n\n/**\n* Gets the desired element from a mouse event\n* @param evt - Event object from the mouse event\n* @returns DOM element we want\n*/\nconst getMouseTarget = this.getMouseTarget = function (evt) {\n if (evt == null) {\n return null;\n }\n let mouseTarget = evt.target;\n\n // if it was a , Opera and WebKit return the SVGElementInstance\n if (mouseTarget.correspondingUseElement) { mouseTarget = mouseTarget.correspondingUseElement; }\n\n // for foreign content, go up until we find the foreignObject\n // WebKit browsers set the mouse target to the svgcanvas div\n if ([NS.MATH, NS.HTML].includes(mouseTarget.namespaceURI) &&\n mouseTarget.id !== 'svgcanvas'\n ) {\n while (mouseTarget.nodeName !== 'foreignObject') {\n mouseTarget = mouseTarget.parentNode;\n if (!mouseTarget) { return svgroot; }\n }\n }\n\n // Get the desired mouseTarget with jQuery selector-fu\n // If it's root-like, select the root\n const currentLayer = getCurrentDrawing().getCurrentLayer();\n if ([svgroot, container, svgcontent, currentLayer].includes(mouseTarget)) {\n return svgroot;\n }\n\n const $target = $(mouseTarget);\n\n // If it's a selection grip, return the grip parent\n if ($target.closest('#selectorParentGroup').length) {\n // While we could instead have just returned mouseTarget,\n // this makes it easier to indentify as being a selector grip\n return selectorManager.selectorParentGroup;\n }\n\n while (mouseTarget.parentNode !== (currentGroup || currentLayer)) {\n mouseTarget = mouseTarget.parentNode;\n }\n\n //\n // // go up until we hit a child of a layer\n // while (mouseTarget.parentNode.parentNode.tagName == 'g') {\n // mouseTarget = mouseTarget.parentNode;\n // }\n // Webkit bubbles the mouse event all the way up to the div, so we\n // set the mouseTarget to the svgroot like the other browsers\n // if (mouseTarget.nodeName.toLowerCase() == 'div') {\n // mouseTarget = svgroot;\n // }\n\n return mouseTarget;\n};\n\ncanvas.pathActions = pathActions;\nfunction resetD (p) {\n p.setAttribute('d', pathActions.convertPath(p));\n}\npathModule.init({\n selectorManager, // Ok since not changing\n canvas, // Ok since not changing\n call,\n resetD,\n round,\n clearSelection,\n addToSelection,\n addCommandToHistory,\n remapElement,\n addSvgElementFromJson,\n getGridSnapping,\n getOpacity,\n getSelectedElements,\n getContainer () {\n return container;\n },\n setStarted (s) {\n started = s;\n },\n getRubberBox () {\n return rubberBox;\n },\n setRubberBox (rb) {\n rubberBox = rb;\n return rubberBox;\n },\n addPtsToSelection ({closedSubpath, grips}) {\n // TODO: Correct this:\n pathActions.canDeleteNodes = true;\n pathActions.closed_subpath = closedSubpath;\n call('selected', grips);\n },\n endChanges ({cmd, elem}) {\n addCommandToHistory(cmd);\n call('changed', [elem]);\n },\n getCurrentZoom,\n getId,\n getNextId,\n getMouseTarget,\n getCurrentMode () {\n return currentMode;\n },\n setCurrentmode (cm) {\n currentMode = cm;\n return currentMode;\n },\n getDrawnPath () {\n return drawnPath;\n },\n setDrawnPath (dp) {\n drawnPath = dp;\n return drawnPath;\n },\n getSVGRoot () { return svgroot; }\n});\n\n// Interface strings, usually for title elements\nconst uiStrings = {};\n\nconst visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';\nconst refAttrs = ['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'];\n\nconst elData = $.data;\n\n// Animation element to change the opacity of any newly created element\nconst opacAni = document.createElementNS(NS.SVG, 'animate');\n$(opacAni).attr({\n attributeName: 'opacity',\n begin: 'indefinite',\n dur: 1,\n fill: 'freeze'\n}).appendTo(svgroot);\n\nconst restoreRefElems = function (elem) {\n // Look for missing reference elements, restore any found\n const attrs = $(elem).attr(refAttrs);\n for (const o in attrs) {\n const val = attrs[o];\n if (val && val.startsWith('url(')) {\n const id = getUrlFromAttr(val).substr(1);\n const ref = getElem(id);\n if (!ref) {\n findDefs().appendChild(removedElements[id]);\n delete removedElements[id];\n }\n }\n }\n\n const childs = elem.getElementsByTagName('*');\n\n if (childs.length) {\n for (let i = 0, l = childs.length; i < l; i++) {\n restoreRefElems(childs[i]);\n }\n }\n};\n\n// (function () {\n// TODO For Issue 208: this is a start on a thumbnail\n// const svgthumb = svgdoc.createElementNS(NS.SVG, 'use');\n// svgthumb.setAttribute('width', '100');\n// svgthumb.setAttribute('height', '100');\n// setHref(svgthumb, '#svgcontent');\n// svgroot.appendChild(svgthumb);\n// }());\n\n// Object to contain image data for raster images that were found encodable\nconst encodableImages = {},\n\n // Object with save options\n saveOptions = {round_digits: 5},\n\n // Object with IDs for imported files, to see if one was already added\n importIds = {},\n\n // Current text style properties\n curText = allProperties.text,\n\n // Object to contain all included extensions\n extensions = {},\n\n // Map of deleted reference elements\n removedElements = {};\n\nlet\n // String with image URL of last loadable image\n lastGoodImgUrl = curConfig.imgPath + 'logo.png',\n\n // Boolean indicating whether or not a draw action has been started\n started = false,\n\n // String with an element's initial transform attribute value\n startTransform = null,\n\n // String indicating the current editor mode\n currentMode = 'select',\n\n // String with the current direction in which an element is being resized\n currentResizeMode = 'none',\n\n // Current general properties\n curProperties = curShape,\n\n // Array with selected elements' Bounding box object\n // selectedBBoxes = new Array(1),\n\n // The DOM element that was just selected\n justSelected = null,\n\n // DOM element for selection rectangle drawn by the user\n rubberBox = null,\n\n // Array of current BBoxes, used in getIntersectionList().\n curBBoxes = [],\n\n // Canvas point for the most recent right click\n lastClickPoint = null;\n\n// Should this return an array by default, so extension results aren't overwritten?\nconst runExtensions = this.runExtensions = function (action, vars, returnArray) {\n let result = returnArray ? [] : false;\n $.each(extensions, function (name, opts) {\n if (opts && action in opts) {\n if (returnArray) {\n result.push(opts[action](vars));\n } else {\n result = opts[action](vars);\n }\n }\n });\n return result;\n};\n\n/**\n* Add an extension to the editor\n* @param {String} name - String with the ID of the extension\n* @param {Function} extFunc - Function supplied by the extension with its data\n*/\nthis.addExtension = function (name, extFunc) {\n let ext;\n if (!(name in extensions)) {\n // Provide private vars/funcs here. Is there a better way to do this?\n const argObj = $.extend(canvas.getPrivateMethods(), {\n svgroot,\n svgcontent,\n nonce: getCurrentDrawing().getNonce(),\n selectorManager\n });\n if (typeof extFunc === 'function') {\n ext = extFunc(argObj);\n } else {\n ext = extFunc;\n if (ext.callback) {\n ext.callback = ext.callback.bind(ext, argObj);\n }\n }\n extensions[name] = ext;\n call('extension_added', ext);\n } else {\n console.log('Cannot add extension \"' + name + '\", an extension by that name already exists.');\n }\n};\n\n// This method sends back an array or a NodeList full of elements that\n// intersect the multi-select rubber-band-box on the currentLayer only.\n//\n// We brute-force getIntersectionList for browsers that do not support it (Firefox).\n//\n// Reference:\n// Firefox does not implement getIntersectionList(), see https://bugzilla.mozilla.org/show_bug.cgi?id=501421\nconst getIntersectionList = this.getIntersectionList = function (rect) {\n if (rubberBox == null) { return null; }\n\n const parent = currentGroup || getCurrentDrawing().getCurrentLayer();\n\n let rubberBBox;\n if (!rect) {\n rubberBBox = rubberBox.getBBox();\n const bb = svgcontent.createSVGRect();\n\n for (const o in rubberBBox) {\n bb[o] = rubberBBox[o] / currentZoom;\n }\n rubberBBox = bb;\n } else {\n rubberBBox = svgcontent.createSVGRect();\n rubberBBox.x = rect.x;\n rubberBBox.y = rect.y;\n rubberBBox.width = rect.width;\n rubberBBox.height = rect.height;\n }\n\n let resultList = null;\n if (!isIE) {\n if (typeof svgroot.getIntersectionList === 'function') {\n // Offset the bbox of the rubber box by the offset of the svgcontent element.\n rubberBBox.x += parseInt(svgcontent.getAttribute('x'), 10);\n rubberBBox.y += parseInt(svgcontent.getAttribute('y'), 10);\n\n resultList = svgroot.getIntersectionList(rubberBBox, parent);\n }\n }\n\n if (resultList == null || typeof resultList.item !== 'function') {\n resultList = [];\n\n if (!curBBoxes.length) {\n // Cache all bboxes\n curBBoxes = getVisibleElementsAndBBoxes(parent);\n }\n let i = curBBoxes.length;\n while (i--) {\n if (!rubberBBox.width) { continue; }\n if (rectsIntersect(rubberBBox, curBBoxes[i].bbox)) {\n resultList.push(curBBoxes[i].elem);\n }\n }\n }\n\n // addToSelection expects an array, but it's ok to pass a NodeList\n // because using square-bracket notation is allowed:\n // https://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html\n return resultList;\n};\n\nthis.getStrokedBBox = getStrokedBBoxDefaultVisible;\n\nthis.getVisibleElements = getVisibleElements;\n\n/**\n* Get all elements that have a BBox (excludes <defs>, <title>, etc).\n* Note that 0-opacity, off-screen etc elements are still considered \"visible\"\n* for this function\n* @param parent - The parent DOM element to search within\n* @returns {Array} An array with objects that include:\n* - elem - The element\n* - bbox - The element's BBox as retrieved from `getStrokedBBoxDefaultVisible`\n*/\nconst getVisibleElementsAndBBoxes = this.getVisibleElementsAndBBoxes = function (parent) {\n if (!parent) {\n parent = $(svgcontent).children(); // Prevent layers from being included\n }\n const contentElems = [];\n $(parent).children().each(function (i, elem) {\n if (elem.getBBox) {\n contentElems.push({elem, bbox: getStrokedBBoxDefaultVisible([elem])});\n }\n });\n return contentElems.reverse();\n};\n\n/**\n* Wrap an SVG element into a group element, mark the group as 'gsvg'\n* @param elem - SVG element to wrap\n*/\nconst groupSvgElem = this.groupSvgElem = function (elem) {\n const g = document.createElementNS(NS.SVG, 'g');\n elem.parentNode.replaceChild(g, elem);\n $(g).append(elem).data('gsvg', elem)[0].id = getNextId();\n};\n\n// Set scope for these functions\n\n// Object to contain editor event names and callback functions\nconst events = {};\n\ncanvas.call = call;\n\n/**\n* Attaches a callback function to an event\n* @param {String} ev - String indicating the name of the event\n* @param {Function} f - The callback function to bind to the event\n* @returns The previous event\n*/\ncanvas.bind = function (ev, f) {\n const old = events[ev];\n events[ev] = f;\n return old;\n};\n\n/**\n* Runs the SVG Document through the sanitizer and then updates its paths.\n* @param newDoc - The SVG DOM document\n*/\nthis.prepareSvg = function (newDoc) {\n this.sanitizeSvg(newDoc.documentElement);\n\n // convert paths into absolute commands\n const paths = newDoc.getElementsByTagNameNS(NS.SVG, 'path');\n for (let i = 0, len = paths.length; i < len; ++i) {\n const path = paths[i];\n path.setAttribute('d', pathActions.convertPath(path));\n pathActions.fixEnd(path);\n }\n};\n\n/**\n* Hack for Firefox bugs where text element features aren't updated or get\n* messed up. See issue 136 and issue 137.\n* This function clones the element and re-selects it\n* @todo Test for this bug on load and add it to \"support\" object instead of\n* browser sniffing\n* @param elem - The (text) DOM element to clone\n* @returns Cloned element\n*/\nconst ffClone = function (elem) {\n if (!isGecko()) { return elem; }\n const clone = elem.cloneNode(true);\n elem.parentNode.insertBefore(clone, elem);\n elem.parentNode.removeChild(elem);\n selectorManager.releaseSelector(elem);\n selectedElements[0] = clone;\n selectorManager.requestSelector(clone).showGrips(true);\n return clone;\n};\n\n// this.each is deprecated, if any extension used this it can be recreated by doing this:\n// $(canvas.getRootElem()).children().each(...)\n\n// this.each = function (cb) {\n// $(svgroot).children().each(cb);\n// };\n\n/**\n* Removes any old rotations if present, prepends a new rotation at the\n* transformed center\n* @param val - The new rotation angle in degrees\n* @param {Boolean} preventUndo - Indicates whether the action should be undoable or not\n*/\nthis.setRotationAngle = function (val, preventUndo) {\n // ensure val is the proper type\n val = parseFloat(val);\n const elem = selectedElements[0];\n const oldTransform = elem.getAttribute('transform');\n const bbox = utilsGetBBox(elem);\n const cx = bbox.x + bbox.width / 2, cy = bbox.y + bbox.height / 2;\n const tlist = getTransformList(elem);\n\n // only remove the real rotational transform if present (i.e. at index=0)\n if (tlist.numberOfItems > 0) {\n const xform = tlist.getItem(0);\n if (xform.type === 4) {\n tlist.removeItem(0);\n }\n }\n // find Rnc and insert it\n if (val !== 0) {\n const center = transformPoint(cx, cy, transformListToTransform(tlist).matrix);\n const Rnc = svgroot.createSVGTransform();\n Rnc.setRotate(val, center.x, center.y);\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(Rnc, 0);\n } else {\n tlist.appendItem(Rnc);\n }\n } else if (tlist.numberOfItems === 0) {\n elem.removeAttribute('transform');\n }\n\n if (!preventUndo) {\n // we need to undo it, then redo it so it can be undo-able! :)\n // TODO: figure out how to make changes to transform list undo-able cross-browser?\n const newTransform = elem.getAttribute('transform');\n elem.setAttribute('transform', oldTransform);\n changeSelectedAttribute('transform', newTransform, selectedElements);\n call('changed', selectedElements);\n }\n // const pointGripContainer = getElem('pathpointgrip_container');\n // if (elem.nodeName === 'path' && pointGripContainer) {\n // pathActions.setPointContainerTransform(elem.getAttribute('transform'));\n // }\n const selector = selectorManager.requestSelector(selectedElements[0]);\n selector.resize();\n selector.updateGripCursors(val);\n};\n\n// Runs recalculateDimensions on the selected elements,\n// adding the changes to a single batch command\nconst recalculateAllSelectedDimensions = this.recalculateAllSelectedDimensions = function () {\n const text = (currentResizeMode === 'none' ? 'position' : 'size');\n const batchCmd = new BatchCommand(text);\n\n let i = selectedElements.length;\n while (i--) {\n const elem = selectedElements[i];\n // if (getRotationAngle(elem) && !hasMatrixTransform(getTransformList(elem))) { continue; }\n const cmd = recalculateDimensions(elem);\n if (cmd) {\n batchCmd.addSubCommand(cmd);\n }\n }\n\n if (!batchCmd.isEmpty()) {\n addCommandToHistory(batchCmd);\n call('changed', selectedElements);\n }\n};\n\n// Debug tool to easily see the current matrix in the browser's console\nconst logMatrix = function (m) {\n console.log([m.a, m.b, m.c, m.d, m.e, m.f]);\n};\n\n// Root Current Transformation Matrix in user units\nlet rootSctm = null;\n\n/**\n* Group: Selection\n*/\n\nthis.clearSelection = clearSelection;\n\n// TODO: do we need to worry about selectedBBoxes here?\n\nthis.addToSelection = addToSelection;\n\n/**\n* Selects only the given elements, shortcut for clearSelection(); addToSelection()\n* @param {Array} elems - an array of DOM elements to be selected\n*/\nconst selectOnly = this.selectOnly = function (elems, showGrips) {\n clearSelection(true);\n addToSelection(elems, showGrips);\n};\n\n// TODO: could use slice here to make this faster?\n// TODO: should the 'selected' handler\n\n/**\n* Removes elements from the selection.\n* @param {Array} elemsToRemove - an array of elements to remove from selection\n*/\n/* const removeFromSelection = */ this.removeFromSelection = function (elemsToRemove) {\n if (selectedElements[0] == null) { return; }\n if (!elemsToRemove.length) { return; }\n\n // find every element and remove it from our array copy\n let j = 0;\n const newSelectedItems = [],\n len = selectedElements.length;\n newSelectedItems.length = len;\n for (let i = 0; i < len; ++i) {\n const elem = selectedElements[i];\n if (elem) {\n // keep the item\n if (!elemsToRemove.includes(elem)) {\n newSelectedItems[j] = elem;\n j++;\n } else { // remove the item and its selector\n selectorManager.releaseSelector(elem);\n }\n }\n }\n // the copy becomes the master now\n selectedElements = newSelectedItems;\n};\n\n// Clears the selection, then adds all elements in the current layer to the selection.\nthis.selectAllInCurrentLayer = function () {\n const currentLayer = getCurrentDrawing().getCurrentLayer();\n if (currentLayer) {\n currentMode = 'select';\n selectOnly($(currentGroup || currentLayer).children());\n }\n};\n\nlet drawnPath = null;\n\n// Mouse events\n(function () {\nconst freehand = {\n minx: null,\n miny: null,\n maxx: null,\n maxy: null\n};\nconst THRESHOLD_DIST = 0.8,\n STEP_COUNT = 10;\nlet dAttr = null,\n startX = null,\n startY = null,\n rStartX = null,\n rStartY = null,\n initBbox = {},\n sumDistance = 0,\n controllPoint2 = {x: 0, y: 0},\n controllPoint1 = {x: 0, y: 0},\n start = {x: 0, y: 0},\n end = {x: 0, y: 0},\n bSpline = {x: 0, y: 0},\n nextPos = {x: 0, y: 0},\n parameter,\n nextParameter;\n\nconst getBsplinePoint = function (t) {\n const spline = {x: 0, y: 0},\n p0 = controllPoint2,\n p1 = controllPoint1,\n p2 = start,\n p3 = end,\n S = 1.0 / 6.0,\n t2 = t * t,\n t3 = t2 * t;\n\n const m = [\n [-1, 3, -3, 1],\n [3, -6, 3, 0],\n [-3, 0, 3, 0],\n [1, 4, 1, 0]\n ];\n\n spline.x = S * (\n (p0.x * m[0][0] + p1.x * m[0][1] + p2.x * m[0][2] + p3.x * m[0][3]) * t3 +\n (p0.x * m[1][0] + p1.x * m[1][1] + p2.x * m[1][2] + p3.x * m[1][3]) * t2 +\n (p0.x * m[2][0] + p1.x * m[2][1] + p2.x * m[2][2] + p3.x * m[2][3]) * t +\n (p0.x * m[3][0] + p1.x * m[3][1] + p2.x * m[3][2] + p3.x * m[3][3])\n );\n spline.y = S * (\n (p0.y * m[0][0] + p1.y * m[0][1] + p2.y * m[0][2] + p3.y * m[0][3]) * t3 +\n (p0.y * m[1][0] + p1.y * m[1][1] + p2.y * m[1][2] + p3.y * m[1][3]) * t2 +\n (p0.y * m[2][0] + p1.y * m[2][1] + p2.y * m[2][2] + p3.y * m[2][3]) * t +\n (p0.y * m[3][0] + p1.y * m[3][1] + p2.y * m[3][2] + p3.y * m[3][3])\n );\n\n return {\n x: spline.x,\n y: spline.y\n };\n};\n// - when we are in a create mode, the element is added to the canvas\n// but the action is not recorded until mousing up\n// - when we are in select mode, select the element, remember the position\n// and do nothing else\nconst mouseDown = function (evt) {\n if (canvas.spaceKey || evt.button === 1) { return; }\n\n const rightClick = evt.button === 2;\n\n if (evt.altKey) { // duplicate when dragging\n canvas.cloneSelectedElements(0, 0);\n }\n\n rootSctm = $('#svgcontent g')[0].getScreenCTM().inverse();\n\n const pt = transformPoint(evt.pageX, evt.pageY, rootSctm),\n mouseX = pt.x * currentZoom,\n mouseY = pt.y * currentZoom;\n\n evt.preventDefault();\n\n if (rightClick) {\n currentMode = 'select';\n lastClickPoint = pt;\n }\n\n // This would seem to be unnecessary...\n // if (!['select', 'resize'].includes(currentMode)) {\n // setGradient();\n // }\n\n let x = mouseX / currentZoom,\n y = mouseY / currentZoom;\n let mouseTarget = getMouseTarget(evt);\n\n if (mouseTarget.tagName === 'a' && mouseTarget.childNodes.length === 1) {\n mouseTarget = mouseTarget.firstChild;\n }\n\n // realX/y ignores grid-snap value\n const realX = x;\n rStartX = startX = x;\n const realY = y;\n rStartY = startY = y;\n\n if (curConfig.gridSnapping) {\n x = snapToGrid(x);\n y = snapToGrid(y);\n startX = snapToGrid(startX);\n startY = snapToGrid(startY);\n }\n\n // if it is a selector grip, then it must be a single element selected,\n // set the mouseTarget to that and update the mode to rotate/resize\n\n if (mouseTarget === selectorManager.selectorParentGroup && selectedElements[0] != null) {\n const grip = evt.target;\n const griptype = elData(grip, 'type');\n // rotating\n if (griptype === 'rotate') {\n currentMode = 'rotate';\n // resizing\n } else if (griptype === 'resize') {\n currentMode = 'resize';\n currentResizeMode = elData(grip, 'dir');\n }\n mouseTarget = selectedElements[0];\n }\n\n startTransform = mouseTarget.getAttribute('transform');\n let i, strokeW;\n const tlist = getTransformList(mouseTarget);\n switch (currentMode) {\n case 'select':\n started = true;\n currentResizeMode = 'none';\n if (rightClick) { started = false; }\n\n if (mouseTarget !== svgroot) {\n // if this element is not yet selected, clear selection and select it\n if (!selectedElements.includes(mouseTarget)) {\n // only clear selection if shift is not pressed (otherwise, add\n // element to selection)\n if (!evt.shiftKey) {\n // No need to do the call here as it will be done on addToSelection\n clearSelection(true);\n }\n addToSelection([mouseTarget]);\n justSelected = mouseTarget;\n pathActions.clear();\n }\n // else if it's a path, go into pathedit mode in mouseup\n\n if (!rightClick) {\n // insert a dummy transform so if the element(s) are moved it will have\n // a transform to use for its translate\n for (i = 0; i < selectedElements.length; ++i) {\n if (selectedElements[i] == null) { continue; }\n const slist = getTransformList(selectedElements[i]);\n if (slist.numberOfItems) {\n slist.insertItemBefore(svgroot.createSVGTransform(), 0);\n } else {\n slist.appendItem(svgroot.createSVGTransform());\n }\n }\n }\n } else if (!rightClick) {\n clearSelection();\n currentMode = 'multiselect';\n if (rubberBox == null) {\n rubberBox = selectorManager.getRubberBandBox();\n }\n rStartX *= currentZoom;\n rStartY *= currentZoom;\n // console.log('p',[evt.pageX, evt.pageY]);\n // console.log('c',[evt.clientX, evt.clientY]);\n // console.log('o',[evt.offsetX, evt.offsetY]);\n // console.log('s',[startX, startY]);\n\n assignAttributes(rubberBox, {\n x: rStartX,\n y: rStartY,\n width: 0,\n height: 0,\n display: 'inline'\n }, 100);\n }\n break;\n case 'zoom':\n started = true;\n if (rubberBox == null) {\n rubberBox = selectorManager.getRubberBandBox();\n }\n assignAttributes(rubberBox, {\n x: realX * currentZoom,\n y: realX * currentZoom,\n width: 0,\n height: 0,\n display: 'inline'\n }, 100);\n break;\n case 'resize':\n started = true;\n startX = x;\n startY = y;\n\n // Getting the BBox from the selection box, since we know we\n // want to orient around it\n initBbox = utilsGetBBox($('#selectedBox0')[0]);\n const bb = {};\n $.each(initBbox, function (key, val) {\n bb[key] = val / currentZoom;\n });\n initBbox = bb;\n\n // append three dummy transforms to the tlist so that\n // we can translate,scale,translate in mousemove\n const pos = getRotationAngle(mouseTarget) ? 1 : 0;\n\n if (hasMatrixTransform(tlist)) {\n tlist.insertItemBefore(svgroot.createSVGTransform(), pos);\n tlist.insertItemBefore(svgroot.createSVGTransform(), pos);\n tlist.insertItemBefore(svgroot.createSVGTransform(), pos);\n } else {\n tlist.appendItem(svgroot.createSVGTransform());\n tlist.appendItem(svgroot.createSVGTransform());\n tlist.appendItem(svgroot.createSVGTransform());\n\n if (supportsNonScalingStroke()) {\n // Handle crash for newer Chrome and Safari 6 (Mobile and Desktop):\n // https://code.google.com/p/svg-edit/issues/detail?id=904\n // Chromium issue: https://code.google.com/p/chromium/issues/detail?id=114625\n // TODO: Remove this workaround once vendor fixes the issue\n const iswebkit = isWebkit();\n\n let delayedStroke;\n if (iswebkit) {\n delayedStroke = function (ele) {\n const _stroke = ele.getAttributeNS(null, 'stroke');\n ele.removeAttributeNS(null, 'stroke');\n // Re-apply stroke after delay. Anything higher than 1 seems to cause flicker\n if (_stroke !== null) setTimeout(function () { ele.setAttributeNS(null, 'stroke', _stroke); }, 0);\n };\n }\n mouseTarget.style.vectorEffect = 'non-scaling-stroke';\n if (iswebkit) { delayedStroke(mouseTarget); }\n\n const all = mouseTarget.getElementsByTagName('*'),\n len = all.length;\n for (i = 0; i < len; i++) {\n all[i].style.vectorEffect = 'non-scaling-stroke';\n if (iswebkit) { delayedStroke(all[i]); }\n }\n }\n }\n break;\n case 'fhellipse':\n case 'fhrect':\n case 'fhpath':\n start.x = realX;\n start.y = realY;\n started = true;\n dAttr = realX + ',' + realY + ' ';\n strokeW = parseFloat(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width;\n addSvgElementFromJson({\n element: 'polyline',\n curStyles: true,\n attr: {\n points: dAttr,\n id: getNextId(),\n fill: 'none',\n opacity: curShape.opacity / 2,\n 'stroke-linecap': 'round',\n style: 'pointer-events:none'\n }\n });\n freehand.minx = realX;\n freehand.maxx = realX;\n freehand.miny = realY;\n freehand.maxy = realY;\n break;\n case 'image':\n started = true;\n const newImage = addSvgElementFromJson({\n element: 'image',\n attr: {\n x,\n y,\n width: 0,\n height: 0,\n id: getNextId(),\n opacity: curShape.opacity / 2,\n style: 'pointer-events:inherit'\n }\n });\n setHref(newImage, lastGoodImgUrl);\n preventClickDefault(newImage);\n break;\n case 'square':\n // FIXME: once we create the rect, we lose information that this was a square\n // (for resizing purposes this could be important)\n // Fallthrough\n case 'rect':\n started = true;\n startX = x;\n startY = y;\n addSvgElementFromJson({\n element: 'rect',\n curStyles: true,\n attr: {\n x,\n y,\n width: 0,\n height: 0,\n id: getNextId(),\n opacity: curShape.opacity / 2\n }\n });\n break;\n case 'line':\n started = true;\n strokeW = Number(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width;\n addSvgElementFromJson({\n element: 'line',\n curStyles: true,\n attr: {\n x1: x,\n y1: y,\n x2: x,\n y2: y,\n id: getNextId(),\n stroke: curShape.stroke,\n 'stroke-width': strokeW,\n 'stroke-dasharray': curShape.stroke_dasharray,\n 'stroke-linejoin': curShape.stroke_linejoin,\n 'stroke-linecap': curShape.stroke_linecap,\n 'stroke-opacity': curShape.stroke_opacity,\n fill: 'none',\n opacity: curShape.opacity / 2,\n style: 'pointer-events:none'\n }\n });\n break;\n case 'circle':\n started = true;\n addSvgElementFromJson({\n element: 'circle',\n curStyles: true,\n attr: {\n cx: x,\n cy: y,\n r: 0,\n id: getNextId(),\n opacity: curShape.opacity / 2\n }\n });\n break;\n case 'ellipse':\n started = true;\n addSvgElementFromJson({\n element: 'ellipse',\n curStyles: true,\n attr: {\n cx: x,\n cy: y,\n rx: 0,\n ry: 0,\n id: getNextId(),\n opacity: curShape.opacity / 2\n }\n });\n break;\n case 'text':\n started = true;\n /* const newText = */ addSvgElementFromJson({\n element: 'text',\n curStyles: true,\n attr: {\n x,\n y,\n id: getNextId(),\n fill: curText.fill,\n 'stroke-width': curText.stroke_width,\n 'font-size': curText.font_size,\n 'font-family': curText.font_family,\n 'text-anchor': 'middle',\n 'xml:space': 'preserve',\n opacity: curShape.opacity\n }\n });\n // newText.textContent = 'text';\n break;\n case 'path':\n // Fall through\n case 'pathedit':\n startX *= currentZoom;\n startY *= currentZoom;\n pathActions.mouseDown(evt, mouseTarget, startX, startY);\n started = true;\n break;\n case 'textedit':\n startX *= currentZoom;\n startY *= currentZoom;\n textActions.mouseDown(evt, mouseTarget, startX, startY);\n started = true;\n break;\n case 'rotate':\n started = true;\n // we are starting an undoable change (a drag-rotation)\n canvas.undoMgr.beginUndoableChange('transform', selectedElements);\n break;\n default:\n // This could occur in an extension\n break;\n }\n\n const extResult = runExtensions('mouseDown', {\n event: evt,\n start_x: startX,\n start_y: startY,\n selectedElements\n }, true);\n\n $.each(extResult, function (i, r) {\n if (r && r.started) {\n started = true;\n }\n });\n};\n\n// in this function we do not record any state changes yet (but we do update\n// any elements that are still being created, moved or resized on the canvas)\nconst mouseMove = function (evt) {\n if (!started) { return; }\n if (evt.button === 1 || canvas.spaceKey) { return; }\n\n let i, xya, c, cx, cy, dx, dy, len, angle, box,\n selected = selectedElements[0];\n const\n pt = transformPoint(evt.pageX, evt.pageY, rootSctm),\n mouseX = pt.x * currentZoom,\n mouseY = pt.y * currentZoom,\n shape = getElem(getId());\n\n let realX = mouseX / currentZoom;\n let x = realX;\n let realY = mouseY / currentZoom;\n let y = realY;\n\n if (curConfig.gridSnapping) {\n x = snapToGrid(x);\n y = snapToGrid(y);\n }\n\n evt.preventDefault();\n let tlist;\n switch (currentMode) {\n case 'select': {\n // we temporarily use a translate on the element(s) being dragged\n // this transform is removed upon mousing up and the element is\n // relocated to the new location\n if (selectedElements[0] !== null) {\n dx = x - startX;\n dy = y - startY;\n\n if (curConfig.gridSnapping) {\n dx = snapToGrid(dx);\n dy = snapToGrid(dy);\n }\n\n if (evt.shiftKey) {\n xya = snapToAngle(startX, startY, x, y);\n ({x, y} = xya);\n }\n\n if (dx !== 0 || dy !== 0) {\n len = selectedElements.length;\n for (i = 0; i < len; ++i) {\n selected = selectedElements[i];\n if (selected == null) { break; }\n // if (i === 0) {\n // const box = utilsGetBBox(selected);\n // selectedBBoxes[i].x = box.x + dx;\n // selectedBBoxes[i].y = box.y + dy;\n // }\n\n // update the dummy transform in our transform list\n // to be a translate\n const xform = svgroot.createSVGTransform();\n tlist = getTransformList(selected);\n // Note that if Webkit and there's no ID for this\n // element, the dummy transform may have gotten lost.\n // This results in unexpected behaviour\n\n xform.setTranslate(dx, dy);\n if (tlist.numberOfItems) {\n tlist.replaceItem(xform, 0);\n } else {\n tlist.appendItem(xform);\n }\n\n // update our internal bbox that we're tracking while dragging\n selectorManager.requestSelector(selected).resize();\n }\n\n call('transition', selectedElements);\n }\n }\n break;\n } case 'multiselect': {\n realX *= currentZoom;\n realY *= currentZoom;\n assignAttributes(rubberBox, {\n x: Math.min(rStartX, realX),\n y: Math.min(rStartY, realY),\n width: Math.abs(realX - rStartX),\n height: Math.abs(realY - rStartY)\n }, 100);\n\n // for each selected:\n // - if newList contains selected, do nothing\n // - if newList doesn't contain selected, remove it from selected\n // - for any newList that was not in selectedElements, add it to selected\n const elemsToRemove = selectedElements.slice(), elemsToAdd = [],\n newList = getIntersectionList();\n\n // For every element in the intersection, add if not present in selectedElements.\n len = newList.length;\n for (i = 0; i < len; ++i) {\n const intElem = newList[i];\n // Found an element that was not selected before, so we should add it.\n if (!selectedElements.includes(intElem)) {\n elemsToAdd.push(intElem);\n }\n // Found an element that was already selected, so we shouldn't remove it.\n const foundInd = elemsToRemove.indexOf(intElem);\n if (foundInd !== -1) {\n elemsToRemove.splice(foundInd, 1);\n }\n }\n\n if (elemsToRemove.length > 0) {\n canvas.removeFromSelection(elemsToRemove);\n }\n\n if (elemsToAdd.length > 0) {\n canvas.addToSelection(elemsToAdd);\n }\n\n break;\n } case 'resize': {\n // we track the resize bounding box and translate/scale the selected element\n // while the mouse is down, when mouse goes up, we use this to recalculate\n // the shape's coordinates\n tlist = getTransformList(selected);\n const hasMatrix = hasMatrixTransform(tlist);\n box = hasMatrix ? initBbox : utilsGetBBox(selected);\n let left = box.x,\n top = box.y,\n {width, height} = box;\n dx = (x - startX);\n dy = (y - startY);\n\n if (curConfig.gridSnapping) {\n dx = snapToGrid(dx);\n dy = snapToGrid(dy);\n height = snapToGrid(height);\n width = snapToGrid(width);\n }\n\n // if rotated, adjust the dx,dy values\n angle = getRotationAngle(selected);\n if (angle) {\n const r = Math.sqrt(dx * dx + dy * dy),\n theta = Math.atan2(dy, dx) - angle * Math.PI / 180.0;\n dx = r * Math.cos(theta);\n dy = r * Math.sin(theta);\n }\n\n // if not stretching in y direction, set dy to 0\n // if not stretching in x direction, set dx to 0\n if (!currentResizeMode.includes('n') && !currentResizeMode.includes('s')) {\n dy = 0;\n }\n if (!currentResizeMode.includes('e') && !currentResizeMode.includes('w')) {\n dx = 0;\n }\n\n let // ts = null,\n tx = 0, ty = 0,\n sy = height ? (height + dy) / height : 1,\n sx = width ? (width + dx) / width : 1;\n // if we are dragging on the north side, then adjust the scale factor and ty\n if (currentResizeMode.includes('n')) {\n sy = height ? (height - dy) / height : 1;\n ty = height;\n }\n\n // if we dragging on the east side, then adjust the scale factor and tx\n if (currentResizeMode.includes('w')) {\n sx = width ? (width - dx) / width : 1;\n tx = width;\n }\n\n // update the transform list with translate,scale,translate\n const translateOrigin = svgroot.createSVGTransform(),\n scale = svgroot.createSVGTransform(),\n translateBack = svgroot.createSVGTransform();\n\n if (curConfig.gridSnapping) {\n left = snapToGrid(left);\n tx = snapToGrid(tx);\n top = snapToGrid(top);\n ty = snapToGrid(ty);\n }\n\n translateOrigin.setTranslate(-(left + tx), -(top + ty));\n if (evt.shiftKey) {\n if (sx === 1) {\n sx = sy;\n } else { sy = sx; }\n }\n scale.setScale(sx, sy);\n\n translateBack.setTranslate(left + tx, top + ty);\n if (hasMatrix) {\n const diff = angle ? 1 : 0;\n tlist.replaceItem(translateOrigin, 2 + diff);\n tlist.replaceItem(scale, 1 + diff);\n tlist.replaceItem(translateBack, Number(diff));\n } else {\n const N = tlist.numberOfItems;\n tlist.replaceItem(translateBack, N - 3);\n tlist.replaceItem(scale, N - 2);\n tlist.replaceItem(translateOrigin, N - 1);\n }\n\n selectorManager.requestSelector(selected).resize();\n\n call('transition', selectedElements);\n\n break;\n } case 'zoom': {\n realX *= currentZoom;\n realY *= currentZoom;\n assignAttributes(rubberBox, {\n x: Math.min(rStartX * currentZoom, realX),\n y: Math.min(rStartY * currentZoom, realY),\n width: Math.abs(realX - rStartX * currentZoom),\n height: Math.abs(realY - rStartY * currentZoom)\n }, 100);\n break;\n } case 'text': {\n assignAttributes(shape, {\n x,\n y\n }, 1000);\n break;\n } case 'line': {\n if (curConfig.gridSnapping) {\n x = snapToGrid(x);\n y = snapToGrid(y);\n }\n\n let x2 = x;\n let y2 = y;\n\n if (evt.shiftKey) {\n xya = snapToAngle(startX, startY, x2, y2);\n x2 = xya.x;\n y2 = xya.y;\n }\n\n shape.setAttributeNS(null, 'x2', x2);\n shape.setAttributeNS(null, 'y2', y2);\n break;\n } case 'foreignObject':\n // fall through\n case 'square':\n // fall through\n case 'rect':\n // fall through\n case 'image': {\n const square = (currentMode === 'square') || evt.shiftKey;\n let\n w = Math.abs(x - startX),\n h = Math.abs(y - startY);\n let newX, newY;\n if (square) {\n w = h = Math.max(w, h);\n newX = startX < x ? startX : startX - w;\n newY = startY < y ? startY : startY - h;\n } else {\n newX = Math.min(startX, x);\n newY = Math.min(startY, y);\n }\n\n if (curConfig.gridSnapping) {\n w = snapToGrid(w);\n h = snapToGrid(h);\n newX = snapToGrid(newX);\n newY = snapToGrid(newY);\n }\n\n assignAttributes(shape, {\n width: w,\n height: h,\n x: newX,\n y: newY\n }, 1000);\n\n break;\n } case 'circle': {\n c = $(shape).attr(['cx', 'cy']);\n ({cx, cy} = c);\n let rad = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));\n if (curConfig.gridSnapping) {\n rad = snapToGrid(rad);\n }\n shape.setAttributeNS(null, 'r', rad);\n break;\n } case 'ellipse': {\n c = $(shape).attr(['cx', 'cy']);\n ({cx, cy} = c);\n if (curConfig.gridSnapping) {\n x = snapToGrid(x);\n cx = snapToGrid(cx);\n y = snapToGrid(y);\n cy = snapToGrid(cy);\n }\n shape.setAttributeNS(null, 'rx', Math.abs(x - cx));\n const ry = Math.abs(evt.shiftKey ? (x - cx) : (y - cy));\n shape.setAttributeNS(null, 'ry', ry);\n break;\n }\n case 'fhellipse':\n case 'fhrect': {\n freehand.minx = Math.min(realX, freehand.minx);\n freehand.maxx = Math.max(realX, freehand.maxx);\n freehand.miny = Math.min(realY, freehand.miny);\n freehand.maxy = Math.max(realY, freehand.maxy);\n }\n // Fallthrough\n case 'fhpath': {\n // dAttr += + realX + ',' + realY + ' ';\n // shape.setAttributeNS(null, 'points', dAttr);\n end.x = realX; end.y = realY;\n if (controllPoint2.x && controllPoint2.y) {\n for (i = 0; i < STEP_COUNT - 1; i++) {\n parameter = i / STEP_COUNT;\n nextParameter = (i + 1) / STEP_COUNT;\n bSpline = getBsplinePoint(nextParameter);\n nextPos = bSpline;\n bSpline = getBsplinePoint(parameter);\n sumDistance += Math.sqrt((nextPos.x - bSpline.x) * (nextPos.x - bSpline.x) + (nextPos.y - bSpline.y) * (nextPos.y - bSpline.y));\n if (sumDistance > THRESHOLD_DIST) {\n dAttr += +bSpline.x + ',' + bSpline.y + ' ';\n shape.setAttributeNS(null, 'points', dAttr);\n sumDistance -= THRESHOLD_DIST;\n }\n }\n }\n controllPoint2 = {x: controllPoint1.x, y: controllPoint1.y};\n controllPoint1 = {x: start.x, y: start.y};\n start = {x: end.x, y: end.y};\n break;\n // update path stretch line coordinates\n } case 'path': {\n }\n // fall through\n case 'pathedit': {\n x *= currentZoom;\n y *= currentZoom;\n\n if (curConfig.gridSnapping) {\n x = snapToGrid(x);\n y = snapToGrid(y);\n startX = snapToGrid(startX);\n startY = snapToGrid(startY);\n }\n if (evt.shiftKey) {\n const {path} = pathModule;\n let x1, y1;\n if (path) {\n x1 = path.dragging ? path.dragging[0] : startX;\n y1 = path.dragging ? path.dragging[1] : startY;\n } else {\n x1 = startX;\n y1 = startY;\n }\n xya = snapToAngle(x1, y1, x, y);\n ({x, y} = xya);\n }\n\n if (rubberBox && rubberBox.getAttribute('display') !== 'none') {\n realX *= currentZoom;\n realY *= currentZoom;\n assignAttributes(rubberBox, {\n x: Math.min(rStartX * currentZoom, realX),\n y: Math.min(rStartY * currentZoom, realY),\n width: Math.abs(realX - rStartX * currentZoom),\n height: Math.abs(realY - rStartY * currentZoom)\n }, 100);\n }\n pathActions.mouseMove(x, y);\n\n break;\n } case 'textedit': {\n x *= currentZoom;\n y *= currentZoom;\n // if (rubberBox && rubberBox.getAttribute('display') !== 'none') {\n // assignAttributes(rubberBox, {\n // x: Math.min(startX, x),\n // y: Math.min(startY, y),\n // width: Math.abs(x - startX),\n // height: Math.abs(y - startY)\n // }, 100);\n // }\n\n textActions.mouseMove(mouseX, mouseY);\n\n break;\n } case 'rotate': {\n box = utilsGetBBox(selected);\n cx = box.x + box.width / 2;\n cy = box.y + box.height / 2;\n const m = getMatrix(selected),\n center = transformPoint(cx, cy, m);\n cx = center.x;\n cy = center.y;\n angle = ((Math.atan2(cy - y, cx - x) * (180 / Math.PI)) - 90) % 360;\n if (curConfig.gridSnapping) {\n angle = snapToGrid(angle);\n }\n if (evt.shiftKey) { // restrict rotations to nice angles (WRS)\n const snap = 45;\n angle = Math.round(angle / snap) * snap;\n }\n\n canvas.setRotationAngle(angle < -180 ? (360 + angle) : angle, true);\n call('transition', selectedElements);\n break;\n } default:\n break;\n }\n\n runExtensions('mouseMove', {\n event: evt,\n mouse_x: mouseX,\n mouse_y: mouseY,\n selected\n });\n}; // mouseMove()\n\n// - in create mode, the element's opacity is set properly, we create an InsertElementCommand\n// and store it on the Undo stack\n// - in move/resize mode, the element's attributes which were affected by the move/resize are\n// identified, a ChangeElementCommand is created and stored on the stack for those attrs\n// this is done in when we recalculate the selected dimensions()\nconst mouseUp = function (evt) {\n if (evt.button === 2) { return; }\n const tempJustSelected = justSelected;\n justSelected = null;\n if (!started) { return; }\n const pt = transformPoint(evt.pageX, evt.pageY, rootSctm),\n mouseX = pt.x * currentZoom,\n mouseY = pt.y * currentZoom,\n x = mouseX / currentZoom,\n y = mouseY / currentZoom;\n\n let element = getElem(getId());\n let keep = false;\n\n const realX = x;\n const realY = y;\n\n // TODO: Make true when in multi-unit mode\n const useUnit = false; // (curConfig.baseUnit !== 'px');\n started = false;\n let attrs, t;\n switch (currentMode) {\n // intentionally fall-through to select here\n case 'resize':\n case 'multiselect':\n if (rubberBox != null) {\n rubberBox.setAttribute('display', 'none');\n curBBoxes = [];\n }\n currentMode = 'select';\n // Fallthrough\n case 'select':\n if (selectedElements[0] != null) {\n // if we only have one selected element\n if (selectedElements[1] == null) {\n // set our current stroke/fill properties to the element's\n const selected = selectedElements[0];\n switch (selected.tagName) {\n case 'g':\n case 'use':\n case 'image':\n case 'foreignObject':\n break;\n default:\n curProperties.fill = selected.getAttribute('fill');\n curProperties.fill_opacity = selected.getAttribute('fill-opacity');\n curProperties.stroke = selected.getAttribute('stroke');\n curProperties.stroke_opacity = selected.getAttribute('stroke-opacity');\n curProperties.stroke_width = selected.getAttribute('stroke-width');\n curProperties.stroke_dasharray = selected.getAttribute('stroke-dasharray');\n curProperties.stroke_linejoin = selected.getAttribute('stroke-linejoin');\n curProperties.stroke_linecap = selected.getAttribute('stroke-linecap');\n }\n\n if (selected.tagName === 'text') {\n curText.font_size = selected.getAttribute('font-size');\n curText.font_family = selected.getAttribute('font-family');\n }\n selectorManager.requestSelector(selected).showGrips(true);\n\n // This shouldn't be necessary as it was done on mouseDown...\n // call('selected', [selected]);\n }\n // always recalculate dimensions to strip off stray identity transforms\n recalculateAllSelectedDimensions();\n // if it was being dragged/resized\n if (realX !== rStartX || realY !== rStartY) {\n const len = selectedElements.length;\n for (let i = 0; i < len; ++i) {\n if (selectedElements[i] == null) { break; }\n if (!selectedElements[i].firstChild) {\n // Not needed for groups (incorrectly resizes elems), possibly not needed at all?\n selectorManager.requestSelector(selectedElements[i]).resize();\n }\n }\n // no change in position/size, so maybe we should move to pathedit\n } else {\n t = evt.target;\n if (selectedElements[0].nodeName === 'path' && selectedElements[1] == null) {\n pathActions.select(selectedElements[0]);\n // if it was a path\n // else, if it was selected and this is a shift-click, remove it from selection\n } else if (evt.shiftKey) {\n if (tempJustSelected !== t) {\n canvas.removeFromSelection([t]);\n }\n }\n } // no change in mouse position\n\n // Remove non-scaling stroke\n if (supportsNonScalingStroke()) {\n const elem = selectedElements[0];\n if (elem) {\n elem.removeAttribute('style');\n walkTree(elem, function (elem) {\n elem.removeAttribute('style');\n });\n }\n }\n }\n return;\n case 'zoom':\n if (rubberBox != null) {\n rubberBox.setAttribute('display', 'none');\n }\n const factor = evt.shiftKey ? 0.5 : 2;\n call('zoomed', {\n x: Math.min(rStartX, realX),\n y: Math.min(rStartY, realY),\n width: Math.abs(realX - rStartX),\n height: Math.abs(realY - rStartY),\n factor\n });\n return;\n case 'fhpath':\n // Check that the path contains at least 2 points; a degenerate one-point path\n // causes problems.\n // Webkit ignores how we set the points attribute with commas and uses space\n // to separate all coordinates, see https://bugs.webkit.org/show_bug.cgi?id=29870\n sumDistance = 0;\n controllPoint2 = {x: 0, y: 0};\n controllPoint1 = {x: 0, y: 0};\n start = {x: 0, y: 0};\n end = {x: 0, y: 0};\n const coords = element.getAttribute('points');\n const commaIndex = coords.indexOf(',');\n if (commaIndex >= 0) {\n keep = coords.indexOf(',', commaIndex + 1) >= 0;\n } else {\n keep = coords.indexOf(' ', coords.indexOf(' ') + 1) >= 0;\n }\n if (keep) {\n element = pathActions.smoothPolylineIntoPath(element);\n }\n break;\n case 'line':\n attrs = $(element).attr(['x1', 'x2', 'y1', 'y2']);\n keep = (attrs.x1 !== attrs.x2 || attrs.y1 !== attrs.y2);\n break;\n case 'foreignObject':\n case 'square':\n case 'rect':\n case 'image':\n attrs = $(element).attr(['width', 'height']);\n // Image should be kept regardless of size (use inherit dimensions later)\n keep = (attrs.width !== '0' || attrs.height !== '0') || currentMode === 'image';\n break;\n case 'circle':\n keep = (element.getAttribute('r') !== '0');\n break;\n case 'ellipse':\n attrs = $(element).attr(['rx', 'ry']);\n keep = (attrs.rx != null || attrs.ry != null);\n break;\n case 'fhellipse':\n if ((freehand.maxx - freehand.minx) > 0 &&\n (freehand.maxy - freehand.miny) > 0) {\n element = addSvgElementFromJson({\n element: 'ellipse',\n curStyles: true,\n attr: {\n cx: (freehand.minx + freehand.maxx) / 2,\n cy: (freehand.miny + freehand.maxy) / 2,\n rx: (freehand.maxx - freehand.minx) / 2,\n ry: (freehand.maxy - freehand.miny) / 2,\n id: getId()\n }\n });\n call('changed', [element]);\n keep = true;\n }\n break;\n case 'fhrect':\n if ((freehand.maxx - freehand.minx) > 0 &&\n (freehand.maxy - freehand.miny) > 0) {\n element = addSvgElementFromJson({\n element: 'rect',\n curStyles: true,\n attr: {\n x: freehand.minx,\n y: freehand.miny,\n width: (freehand.maxx - freehand.minx),\n height: (freehand.maxy - freehand.miny),\n id: getId()\n }\n });\n call('changed', [element]);\n keep = true;\n }\n break;\n case 'text':\n keep = true;\n selectOnly([element]);\n textActions.start(element);\n break;\n case 'path':\n // set element to null here so that it is not removed nor finalized\n element = null;\n // continue to be set to true so that mouseMove happens\n started = true;\n\n const res = pathActions.mouseUp(evt, element, mouseX, mouseY);\n ({element} = res);\n ({keep} = res);\n break;\n case 'pathedit':\n keep = true;\n element = null;\n pathActions.mouseUp(evt);\n break;\n case 'textedit':\n keep = false;\n element = null;\n textActions.mouseUp(evt, mouseX, mouseY);\n break;\n case 'rotate':\n keep = true;\n element = null;\n currentMode = 'select';\n const batchCmd = canvas.undoMgr.finishUndoableChange();\n if (!batchCmd.isEmpty()) {\n addCommandToHistory(batchCmd);\n }\n // perform recalculation to weed out any stray identity transforms that might get stuck\n recalculateAllSelectedDimensions();\n call('changed', selectedElements);\n break;\n default:\n // This could occur in an extension\n break;\n }\n\n const extResult = runExtensions('mouseUp', {\n event: evt,\n mouse_x: mouseX,\n mouse_y: mouseY\n }, true);\n\n $.each(extResult, function (i, r) {\n if (r) {\n keep = r.keep || keep;\n ({element} = r);\n started = r.started || started;\n }\n });\n\n if (!keep && element != null) {\n getCurrentDrawing().releaseId(getId());\n element.parentNode.removeChild(element);\n element = null;\n\n t = evt.target;\n\n // if this element is in a group, go up until we reach the top-level group\n // just below the layer groups\n // TODO: once we implement links, we also would have to check for elements\n while (t && t.parentNode && t.parentNode.parentNode && t.parentNode.parentNode.tagName === 'g') {\n t = t.parentNode;\n }\n // if we are not in the middle of creating a path, and we've clicked on some shape,\n // then go to Select mode.\n // WebKit returns
    when the canvas is clicked, Firefox/Opera return \n if ((currentMode !== 'path' || !drawnPath) &&\n t && t.parentNode &&\n t.parentNode.id !== 'selectorParentGroup' &&\n t.id !== 'svgcanvas' && t.id !== 'svgroot'\n ) {\n // switch into \"select\" mode if we've clicked on an element\n canvas.setMode('select');\n selectOnly([t], true);\n }\n } else if (element != null) {\n canvas.addedNew = true;\n\n if (useUnit) { convertAttrs(element); }\n\n let aniDur = 0.2;\n let cAni;\n if (opacAni.beginElement && parseFloat(element.getAttribute('opacity')) !== curShape.opacity) {\n cAni = $(opacAni).clone().attr({\n to: curShape.opacity,\n dur: aniDur\n }).appendTo(element);\n try {\n // Fails in FF4 on foreignObject\n cAni[0].beginElement();\n } catch (e) {}\n } else {\n aniDur = 0;\n }\n\n // Ideally this would be done on the endEvent of the animation,\n // but that doesn't seem to be supported in Webkit\n setTimeout(function () {\n if (cAni) { cAni.remove(); }\n element.setAttribute('opacity', curShape.opacity);\n element.setAttribute('style', 'pointer-events:inherit');\n cleanupElement(element);\n if (currentMode === 'path') {\n pathActions.toEditMode(element);\n } else if (curConfig.selectNew) {\n selectOnly([element], true);\n }\n // we create the insert command that is stored on the stack\n // undo means to call cmd.unapply(), redo means to call cmd.apply()\n addCommandToHistory(new InsertElementCommand(element));\n\n call('changed', [element]);\n }, aniDur * 1000);\n }\n\n startTransform = null;\n};\n\nconst dblClick = function (evt) {\n const evtTarget = evt.target;\n const parent = evtTarget.parentNode;\n\n // Do nothing if already in current group\n if (parent === currentGroup) { return; }\n\n let mouseTarget = getMouseTarget(evt);\n const {tagName} = mouseTarget;\n\n if (tagName === 'text' && currentMode !== 'textedit') {\n const pt = transformPoint(evt.pageX, evt.pageY, rootSctm);\n textActions.select(mouseTarget, pt.x, pt.y);\n }\n\n if ((tagName === 'g' || tagName === 'a') &&\n getRotationAngle(mouseTarget)\n ) {\n // TODO: Allow method of in-group editing without having to do\n // this (similar to editing rotated paths)\n\n // Ungroup and regroup\n pushGroupProperties(mouseTarget);\n mouseTarget = selectedElements[0];\n clearSelection(true);\n }\n // Reset context\n if (currentGroup) {\n draw.leaveContext();\n }\n\n if ((parent.tagName !== 'g' && parent.tagName !== 'a') ||\n parent === getCurrentDrawing().getCurrentLayer() ||\n mouseTarget === selectorManager.selectorParentGroup\n ) {\n // Escape from in-group edit\n return;\n }\n draw.setContext(mouseTarget);\n};\n\n// prevent links from being followed in the canvas\nconst handleLinkInCanvas = function (e) {\n e.preventDefault();\n return false;\n};\n\n// Added mouseup to the container here.\n// TODO(codedread): Figure out why after the Closure compiler, the window mouseup is ignored.\n$(container).mousedown(mouseDown).mousemove(mouseMove).click(handleLinkInCanvas).dblclick(dblClick).mouseup(mouseUp);\n// $(window).mouseup(mouseUp);\n\n// TODO(rafaelcastrocouto): User preference for shift key and zoom factor\n$(container).bind('mousewheel DOMMouseScroll', function (e) {\n if (!e.shiftKey) { return; }\n\n e.preventDefault();\n const evt = e.originalEvent;\n\n rootSctm = $('#svgcontent g')[0].getScreenCTM().inverse();\n\n const workarea = $('#workarea');\n\tconst scrbar = 15;\n\tconst rulerwidth = curConfig.showRulers ? 16 : 0;\n\n\t// mouse relative to content area in content pixels\n\tconst pt = transformPoint(evt.pageX, evt.pageY, rootSctm);\n\n\t// full work area width in screen pixels\n\tconst editorFullW = workarea.width();\n\tconst editorFullH = workarea.height();\n\n\t// work area width minus scroll and ruler in screen pixels\n\tconst editorW = editorFullW - scrbar - rulerwidth;\n\tconst editorH = editorFullH - scrbar - rulerwidth;\n\n\t// work area width in content pixels\n\tconst workareaViewW = editorW * rootSctm.a;\n\tconst workareaViewH = editorH * rootSctm.d;\n\n\t// content offset from canvas in screen pixels\n\tconst wOffset = workarea.offset();\n\tconst wOffsetLeft = wOffset['left'] + rulerwidth;\n\tconst wOffsetTop = wOffset['top'] + rulerwidth;\n\n const delta = (evt.wheelDelta) ? evt.wheelDelta : (evt.detail) ? -evt.detail : 0;\n if (!delta) { return; }\n\n let factor = Math.max(3 / 4, Math.min(4 / 3, (delta)));\n\n let wZoom, hZoom;\n\tif (factor > 1) {\n\t\twZoom = Math.ceil(editorW / workareaViewW * factor * 100) / 100;\n\t\thZoom = Math.ceil(editorH / workareaViewH * factor * 100) / 100;\n\t} else {\n\t\twZoom = Math.floor(editorW / workareaViewW * factor * 100) / 100;\n\t\thZoom = Math.floor(editorH / workareaViewH * factor * 100) / 100;\n\t}\n\tlet zoomlevel = Math.min(wZoom, hZoom);\n\tzoomlevel = Math.min(10, Math.max(0.01, zoomlevel));\n\tif (zoomlevel === currentZoom) {\n\t\treturn;\n\t}\n\tfactor = zoomlevel / currentZoom;\n\n\t// top left of workarea in content pixels before zoom\n\tconst topLeftOld = transformPoint(wOffsetLeft, wOffsetTop, rootSctm);\n\n\t// top left of workarea in content pixels after zoom\n\tconst topLeftNew = {\n\t\tx: pt.x - (pt.x - topLeftOld.x) / factor,\n\t\ty: pt.y - (pt.y - topLeftOld.y) / factor\n\t};\n\n\t// top left of workarea in canvas pixels relative to content after zoom\n\tconst topLeftNewCanvas = {\n\t\tx: topLeftNew.x * zoomlevel,\n\t\ty: topLeftNew.y * zoomlevel\n\t};\n\n\t// new center in canvas pixels\n\tconst newCtr = {\n\t\tx: topLeftNewCanvas.x - rulerwidth + editorFullW / 2,\n\t\ty: topLeftNewCanvas.y - rulerwidth + editorFullH / 2\n\t};\n\n\tcanvas.setZoom(zoomlevel);\n\t$('#zoom').val((zoomlevel * 100).toFixed(1));\n\n\tcall('updateCanvas', {center: false, newCtr});\n\tcall('zoomDone');\n});\n}());\n\n/**\n* Group: Text edit functions\n* Functions relating to editing text elements\n*/\nconst textActions = canvas.textActions = (function () {\nlet curtext;\nlet textinput;\nlet cursor;\nlet selblock;\nlet blinker;\nlet chardata = [];\nlet textbb; // , transbb;\nlet matrix;\nlet lastX, lastY;\nlet allowDbl;\n\nfunction setCursor (index) {\n const empty = (textinput.value === '');\n $(textinput).focus();\n\n if (!arguments.length) {\n if (empty) {\n index = 0;\n } else {\n if (textinput.selectionEnd !== textinput.selectionStart) { return; }\n index = textinput.selectionEnd;\n }\n }\n\n const charbb = chardata[index];\n if (!empty) {\n textinput.setSelectionRange(index, index);\n }\n cursor = getElem('text_cursor');\n if (!cursor) {\n cursor = document.createElementNS(NS.SVG, 'line');\n assignAttributes(cursor, {\n id: 'text_cursor',\n stroke: '#333',\n 'stroke-width': 1\n });\n cursor = getElem('selectorParentGroup').appendChild(cursor);\n }\n\n if (!blinker) {\n blinker = setInterval(function () {\n const show = (cursor.getAttribute('display') === 'none');\n cursor.setAttribute('display', show ? 'inline' : 'none');\n }, 600);\n }\n\n const startPt = ptToScreen(charbb.x, textbb.y);\n const endPt = ptToScreen(charbb.x, (textbb.y + textbb.height));\n\n assignAttributes(cursor, {\n x1: startPt.x,\n y1: startPt.y,\n x2: endPt.x,\n y2: endPt.y,\n visibility: 'visible',\n display: 'inline'\n });\n\n if (selblock) { selblock.setAttribute('d', ''); }\n}\n\nfunction setSelection (start, end, skipInput) {\n if (start === end) {\n setCursor(end);\n return;\n }\n\n if (!skipInput) {\n textinput.setSelectionRange(start, end);\n }\n\n selblock = getElem('text_selectblock');\n if (!selblock) {\n selblock = document.createElementNS(NS.SVG, 'path');\n assignAttributes(selblock, {\n id: 'text_selectblock',\n fill: 'green',\n opacity: 0.5,\n style: 'pointer-events:none'\n });\n getElem('selectorParentGroup').appendChild(selblock);\n }\n\n const startbb = chardata[start];\n const endbb = chardata[end];\n\n cursor.setAttribute('visibility', 'hidden');\n\n const tl = ptToScreen(startbb.x, textbb.y),\n tr = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y),\n bl = ptToScreen(startbb.x, textbb.y + textbb.height),\n br = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y + textbb.height);\n\n const dstr = 'M' + tl.x + ',' + tl.y +\n ' L' + tr.x + ',' + tr.y +\n ' ' + br.x + ',' + br.y +\n ' ' + bl.x + ',' + bl.y + 'z';\n\n assignAttributes(selblock, {\n d: dstr,\n display: 'inline'\n });\n}\n\nfunction getIndexFromPoint (mouseX, mouseY) {\n // Position cursor here\n const pt = svgroot.createSVGPoint();\n pt.x = mouseX;\n pt.y = mouseY;\n\n // No content, so return 0\n if (chardata.length === 1) { return 0; }\n // Determine if cursor should be on left or right of character\n let charpos = curtext.getCharNumAtPosition(pt);\n if (charpos < 0) {\n // Out of text range, look at mouse coords\n charpos = chardata.length - 2;\n if (mouseX <= chardata[0].x) {\n charpos = 0;\n }\n } else if (charpos >= chardata.length - 2) {\n charpos = chardata.length - 2;\n }\n const charbb = chardata[charpos];\n const mid = charbb.x + (charbb.width / 2);\n if (mouseX > mid) {\n charpos++;\n }\n return charpos;\n}\n\nfunction setCursorFromPoint (mouseX, mouseY) {\n setCursor(getIndexFromPoint(mouseX, mouseY));\n}\n\nfunction setEndSelectionFromPoint (x, y, apply) {\n const i1 = textinput.selectionStart;\n const i2 = getIndexFromPoint(x, y);\n\n const start = Math.min(i1, i2);\n const end = Math.max(i1, i2);\n setSelection(start, end, !apply);\n}\n\nfunction screenToPt (xIn, yIn) {\n const out = {\n x: xIn,\n y: yIn\n };\n\n out.x /= currentZoom;\n out.y /= currentZoom;\n\n if (matrix) {\n const pt = transformPoint(out.x, out.y, matrix.inverse());\n out.x = pt.x;\n out.y = pt.y;\n }\n\n return out;\n}\n\nfunction ptToScreen (xIn, yIn) {\n const out = {\n x: xIn,\n y: yIn\n };\n\n if (matrix) {\n const pt = transformPoint(out.x, out.y, matrix);\n out.x = pt.x;\n out.y = pt.y;\n }\n\n out.x *= currentZoom;\n out.y *= currentZoom;\n\n return out;\n}\n\n/*\n// Not currently in use\nfunction hideCursor () {\n if (cursor) {\n cursor.setAttribute('visibility', 'hidden');\n }\n}\n*/\n\nfunction selectAll (evt) {\n setSelection(0, curtext.textContent.length);\n $(this).unbind(evt);\n}\n\nfunction selectWord (evt) {\n if (!allowDbl || !curtext) { return; }\n\n const ept = transformPoint(evt.pageX, evt.pageY, rootSctm),\n mouseX = ept.x * currentZoom,\n mouseY = ept.y * currentZoom;\n const pt = screenToPt(mouseX, mouseY);\n\n const index = getIndexFromPoint(pt.x, pt.y);\n const str = curtext.textContent;\n const first = str.substr(0, index).replace(/[a-z0-9]+$/i, '').length;\n const m = str.substr(index).match(/^[a-z0-9]+/i);\n const last = (m ? m[0].length : 0) + index;\n setSelection(first, last);\n\n // Set tripleclick\n $(evt.target).click(selectAll);\n setTimeout(function () {\n $(evt.target).unbind('click', selectAll);\n }, 300);\n}\n\nreturn {\n select (target, x, y) {\n curtext = target;\n textActions.toEditMode(x, y);\n },\n start (elem) {\n curtext = elem;\n textActions.toEditMode();\n },\n mouseDown (evt, mouseTarget, startX, startY) {\n const pt = screenToPt(startX, startY);\n\n textinput.focus();\n setCursorFromPoint(pt.x, pt.y);\n lastX = startX;\n lastY = startY;\n\n // TODO: Find way to block native selection\n },\n mouseMove (mouseX, mouseY) {\n const pt = screenToPt(mouseX, mouseY);\n setEndSelectionFromPoint(pt.x, pt.y);\n },\n mouseUp (evt, mouseX, mouseY) {\n const pt = screenToPt(mouseX, mouseY);\n\n setEndSelectionFromPoint(pt.x, pt.y, true);\n\n // TODO: Find a way to make this work: Use transformed BBox instead of evt.target\n // if (lastX === mouseX && lastY === mouseY\n // && !rectsIntersect(transbb, {x: pt.x, y: pt.y, width: 0, height: 0})) {\n // textActions.toSelectMode(true);\n // }\n\n if (\n evt.target !== curtext &&\n mouseX < lastX + 2 &&\n mouseX > lastX - 2 &&\n mouseY < lastY + 2 &&\n mouseY > lastY - 2\n ) {\n textActions.toSelectMode(true);\n }\n },\n setCursor,\n toEditMode (x, y) {\n allowDbl = false;\n currentMode = 'textedit';\n selectorManager.requestSelector(curtext).showGrips(false);\n // Make selector group accept clicks\n /* const selector = */ selectorManager.requestSelector(curtext); // Do we need this? Has side effect of setting lock, so keeping for now, but next line wasn't being used\n // const sel = selector.selectorRect;\n\n textActions.init();\n\n $(curtext).css('cursor', 'text');\n\n // if (supportsEditableText()) {\n // curtext.setAttribute('editable', 'simple');\n // return;\n // }\n\n if (!arguments.length) {\n setCursor();\n } else {\n const pt = screenToPt(x, y);\n setCursorFromPoint(pt.x, pt.y);\n }\n\n setTimeout(function () {\n allowDbl = true;\n }, 300);\n },\n toSelectMode (selectElem) {\n currentMode = 'select';\n clearInterval(blinker);\n blinker = null;\n if (selblock) { $(selblock).attr('display', 'none'); }\n if (cursor) { $(cursor).attr('visibility', 'hidden'); }\n $(curtext).css('cursor', 'move');\n\n if (selectElem) {\n clearSelection();\n $(curtext).css('cursor', 'move');\n\n call('selected', [curtext]);\n addToSelection([curtext], true);\n }\n if (curtext && !curtext.textContent.length) {\n // No content, so delete\n canvas.deleteSelectedElements();\n }\n\n $(textinput).blur();\n\n curtext = false;\n\n // if (supportsEditableText()) {\n // curtext.removeAttribute('editable');\n // }\n },\n setInputElem (elem) {\n textinput = elem;\n // $(textinput).blur(hideCursor);\n },\n clear () {\n if (currentMode === 'textedit') {\n textActions.toSelectMode();\n }\n },\n init (inputElem) {\n if (!curtext) { return; }\n let i, end;\n // if (supportsEditableText()) {\n // curtext.select();\n // return;\n // }\n\n if (!curtext.parentNode) {\n // Result of the ffClone, need to get correct element\n curtext = selectedElements[0];\n selectorManager.requestSelector(curtext).showGrips(false);\n }\n\n const str = curtext.textContent;\n const len = str.length;\n\n const xform = curtext.getAttribute('transform');\n\n textbb = utilsGetBBox(curtext);\n\n matrix = xform ? getMatrix(curtext) : null;\n\n chardata = [];\n chardata.length = len;\n textinput.focus();\n\n $(curtext).unbind('dblclick', selectWord).dblclick(selectWord);\n\n if (!len) {\n end = {x: textbb.x + (textbb.width / 2), width: 0};\n }\n\n for (i = 0; i < len; i++) {\n const start = curtext.getStartPositionOfChar(i);\n end = curtext.getEndPositionOfChar(i);\n\n if (!supportsGoodTextCharPos()) {\n const offset = canvas.contentW * currentZoom;\n start.x -= offset;\n end.x -= offset;\n\n start.x /= currentZoom;\n end.x /= currentZoom;\n }\n\n // Get a \"bbox\" equivalent for each character. Uses the\n // bbox data of the actual text for y, height purposes\n\n // TODO: Decide if y, width and height are actually necessary\n chardata[i] = {\n x: start.x,\n y: textbb.y, // start.y?\n width: end.x - start.x,\n height: textbb.height\n };\n }\n\n // Add a last bbox for cursor at end of text\n chardata.push({\n x: end.x,\n width: 0\n });\n setSelection(textinput.selectionStart, textinput.selectionEnd, true);\n }\n};\n}());\n\n/**\n* Group: Serialization\n*/\n\n/**\n* Looks at DOM elements inside the to see if they are referred to,\n* removes them from the DOM if they are not.\n* @returns The amount of elements that were removed\n*/\nconst removeUnusedDefElems = this.removeUnusedDefElems = function () {\n const defs = svgcontent.getElementsByTagNameNS(NS.SVG, 'defs');\n if (!defs || !defs.length) { return 0; }\n\n // if (!defs.firstChild) { return; }\n\n const defelemUses = [];\n let numRemoved = 0;\n const attrs = ['fill', 'stroke', 'filter', 'marker-start', 'marker-mid', 'marker-end'];\n const alen = attrs.length;\n\n const allEls = svgcontent.getElementsByTagNameNS(NS.SVG, '*');\n const allLen = allEls.length;\n\n let i, j;\n for (i = 0; i < allLen; i++) {\n const el = allEls[i];\n for (j = 0; j < alen; j++) {\n const ref = getUrlFromAttr(el.getAttribute(attrs[j]));\n if (ref) {\n defelemUses.push(ref.substr(1));\n }\n }\n\n // gradients can refer to other gradients\n const href = getHref(el);\n if (href && href.startsWith('#')) {\n defelemUses.push(href.substr(1));\n }\n }\n\n const defelems = $(defs).find('linearGradient, radialGradient, filter, marker, svg, symbol');\n i = defelems.length;\n while (i--) {\n const defelem = defelems[i];\n const {id} = defelem;\n if (!defelemUses.includes(id)) {\n // Not found, so remove (but remember)\n removedElements[id] = defelem;\n defelem.parentNode.removeChild(defelem);\n numRemoved++;\n }\n }\n\n return numRemoved;\n};\n\n/**\n* Main function to set up the SVG content for output\n* @returns {String} The SVG image for output\n*/\nthis.svgCanvasToString = function () {\n // keep calling it until there are none to remove\n while (removeUnusedDefElems() > 0) {}\n\n pathActions.clear(true);\n\n // Keep SVG-Edit comment on top\n $.each(svgcontent.childNodes, function (i, node) {\n if (i && node.nodeType === 8 && node.data.includes('Created with')) {\n svgcontent.insertBefore(node, svgcontent.firstChild);\n }\n });\n\n // Move out of in-group editing mode\n if (currentGroup) {\n draw.leaveContext();\n selectOnly([currentGroup]);\n }\n\n const nakedSvgs = [];\n\n // Unwrap gsvg if it has no special attributes (only id and style)\n $(svgcontent).find('g:data(gsvg)').each(function () {\n const attrs = this.attributes;\n let len = attrs.length;\n for (let i = 0; i < len; i++) {\n if (attrs[i].nodeName === 'id' || attrs[i].nodeName === 'style') {\n len--;\n }\n }\n // No significant attributes, so ungroup\n if (len <= 0) {\n const svg = this.firstChild;\n nakedSvgs.push(svg);\n $(this).replaceWith(svg);\n }\n });\n const output = this.svgToString(svgcontent, 0);\n\n // Rewrap gsvg\n if (nakedSvgs.length) {\n $(nakedSvgs).each(function () {\n groupSvgElem(this);\n });\n }\n\n return output;\n};\n\n/**\n* Sub function ran on each SVG element to convert it to a string as desired\n* @param elem - The SVG element to convert\n* @param {Number} indent - Integer with the amount of spaces to indent this tag\n* @returns {String} The given element as an SVG tag\n*/\nthis.svgToString = function (elem, indent) {\n const out = [];\n const unit = curConfig.baseUnit;\n const unitRe = new RegExp('^-?[\\\\d\\\\.]+' + unit + '$');\n\n if (elem) {\n cleanupElement(elem);\n const attrs = elem.attributes;\n let attr, i;\n const childs = elem.childNodes;\n\n for (i = 0; i < indent; i++) { out.push(' '); }\n out.push('<'); out.push(elem.nodeName);\n if (elem.id === 'svgcontent') {\n // Process root element separately\n const res = getResolution();\n\n const vb = '';\n // TODO: Allow this by dividing all values by current baseVal\n // Note that this also means we should properly deal with this on import\n // if (curConfig.baseUnit !== 'px') {\n // const unit = curConfig.baseUnit;\n // const unitM = getTypeMap()[unit];\n // res.w = shortFloat(res.w / unitM);\n // res.h = shortFloat(res.h / unitM);\n // vb = ' viewBox=\"' + [0, 0, res.w, res.h].join(' ') + '\"';\n // res.w += unit;\n // res.h += unit;\n // }\n\n if (unit !== 'px') {\n res.w = convertUnit(res.w, unit) + unit;\n res.h = convertUnit(res.h, unit) + unit;\n }\n\n out.push(' width=\"' + res.w + '\" height=\"' + res.h + '\"' + vb + ' xmlns=\"' + NS.SVG + '\"');\n\n const nsuris = {};\n\n // Check elements for namespaces, add if found\n $(elem).find('*').andSelf().each(function () {\n // const el = this;\n // for some elements have no attribute\n const uri = this.namespaceURI;\n if (uri && !nsuris[uri] && nsMap[uri] && nsMap[uri] !== 'xmlns' && nsMap[uri] !== 'xml') {\n nsuris[uri] = true;\n out.push(' xmlns:' + nsMap[uri] + '=\"' + uri + '\"');\n }\n\n $.each(this.attributes, function (i, attr) {\n const uri = attr.namespaceURI;\n if (uri && !nsuris[uri] && nsMap[uri] !== 'xmlns' && nsMap[uri] !== 'xml') {\n nsuris[uri] = true;\n out.push(' xmlns:' + nsMap[uri] + '=\"' + uri + '\"');\n }\n });\n });\n\n i = attrs.length;\n const attrNames = ['width', 'height', 'xmlns', 'x', 'y', 'viewBox', 'id', 'overflow'];\n while (i--) {\n attr = attrs.item(i);\n const attrVal = toXml(attr.value);\n\n // Namespaces have already been dealt with, so skip\n if (attr.nodeName.startsWith('xmlns:')) { continue; }\n\n // only serialize attributes we don't use internally\n if (attrVal !== '' && !attrNames.includes(attr.localName)) {\n if (!attr.namespaceURI || nsMap[attr.namespaceURI]) {\n out.push(' ');\n out.push(attr.nodeName); out.push('=\"');\n out.push(attrVal); out.push('\"');\n }\n }\n }\n } else {\n // Skip empty defs\n if (elem.nodeName === 'defs' && !elem.firstChild) { return; }\n\n const mozAttrs = ['-moz-math-font-style', '_moz-math-font-style'];\n for (i = attrs.length - 1; i >= 0; i--) {\n attr = attrs.item(i);\n let attrVal = toXml(attr.value);\n // remove bogus attributes added by Gecko\n if (mozAttrs.includes(attr.localName)) { continue; }\n if (attrVal !== '') {\n if (attrVal.startsWith('pointer-events')) { continue; }\n if (attr.localName === 'class' && attrVal.startsWith('se_')) { continue; }\n out.push(' ');\n if (attr.localName === 'd') { attrVal = pathActions.convertPath(elem, true); }\n if (!isNaN(attrVal)) {\n attrVal = shortFloat(attrVal);\n } else if (unitRe.test(attrVal)) {\n attrVal = shortFloat(attrVal) + unit;\n }\n\n // Embed images when saving\n if (saveOptions.apply &&\n elem.nodeName === 'image' &&\n attr.localName === 'href' &&\n saveOptions.images &&\n saveOptions.images === 'embed'\n ) {\n const img = encodableImages[attrVal];\n if (img) { attrVal = img; }\n }\n\n // map various namespaces to our fixed namespace prefixes\n // (the default xmlns attribute itself does not get a prefix)\n if (!attr.namespaceURI || attr.namespaceURI === NS.SVG || nsMap[attr.namespaceURI]) {\n out.push(attr.nodeName); out.push('=\"');\n out.push(attrVal); out.push('\"');\n }\n }\n }\n }\n\n if (elem.hasChildNodes()) {\n out.push('>');\n indent++;\n let bOneLine = false;\n\n for (i = 0; i < childs.length; i++) {\n const child = childs.item(i);\n switch (child.nodeType) {\n case 1: // element node\n out.push('\\n');\n out.push(this.svgToString(childs.item(i), indent));\n break;\n case 3: // text node\n const str = child.nodeValue.replace(/^\\s+|\\s+$/g, '');\n if (str !== '') {\n bOneLine = true;\n out.push(String(toXml(str)));\n }\n break;\n case 4: // cdata node\n out.push('\\n');\n out.push(new Array(indent + 1).join(' '));\n out.push('');\n out.push(child.nodeValue);\n out.push('');\n break;\n case 8: // comment\n out.push('\\n');\n out.push(new Array(indent + 1).join(' '));\n out.push('');\n break;\n } // switch on node type\n }\n indent--;\n if (!bOneLine) {\n out.push('\\n');\n for (i = 0; i < indent; i++) { out.push(' '); }\n }\n out.push('');\n } else {\n out.push('/>');\n }\n }\n return out.join('');\n}; // end svgToString()\n\n/**\n* Converts a given image file to a data URL when possible, then runs a given callback\n* @param {String} val - String with the path/URL of the image\n* @param {Function} callback - Optional function to run when image data is found, supplies the\n* result (data URL or false) as first parameter.\n*/\nthis.embedImage = function (val, callback) {\n // load in the image and once it's loaded, get the dimensions\n $(new Image()).load(function () {\n // create a canvas the same size as the raster image\n const canvas = document.createElement('canvas');\n canvas.width = this.width;\n canvas.height = this.height;\n // load the raster image into the canvas\n canvas.getContext('2d').drawImage(this, 0, 0);\n // retrieve the data: URL\n try {\n let urldata = ';svgedit_url=' + encodeURIComponent(val);\n urldata = canvas.toDataURL().replace(';base64', urldata + ';base64');\n encodableImages[val] = urldata;\n } catch (e) {\n encodableImages[val] = false;\n }\n lastGoodImgUrl = val;\n if (callback) { callback(encodableImages[val]); }\n }).attr('src', val);\n};\n\n/**\n* Sets a given URL to be a \"last good image\" URL\n*/\nthis.setGoodImage = function (val) {\n lastGoodImgUrl = val;\n};\n\n/**\n*\n*/\nthis.open = function () {\n // Nothing by default, handled by optional widget/extension\n};\n\n/**\n* Serializes the current drawing into SVG XML text and returns it to the 'saved' handler.\n* This function also includes the XML prolog. Clients of the SvgCanvas bind their save\n* function to the 'saved' event.\n*/\nthis.save = function (opts) {\n // remove the selected outline before serializing\n clearSelection();\n // Update save options if provided\n if (opts) { $.extend(saveOptions, opts); }\n saveOptions.apply = true;\n\n // no need for doctype, see https://jwatt.org/svg/authoring/#doctype-declaration\n const str = this.svgCanvasToString();\n call('saved', str);\n};\n\n/**\n* Codes only is useful for locale-independent detection\n*/\nfunction getIssues ({codesOnly = false} = {}) {\n // remove the selected outline before serializing\n clearSelection();\n\n // Check for known CanVG issues\n const issues = [];\n\n // Selector and notice\n const issueList = {\n feGaussianBlur: uiStrings.exportNoBlur,\n foreignObject: uiStrings.exportNoforeignObject,\n '[stroke-dasharray]': uiStrings.exportNoDashArray\n };\n const content = $(svgcontent);\n\n // Add font/text check if Canvas Text API is not implemented\n if (!('font' in $('')[0].getContext('2d'))) {\n issueList.text = uiStrings.exportNoText;\n }\n\n $.each(issueList, function (sel, descr) {\n if (content.find(sel).length) {\n issues.push(codesOnly ? sel : descr);\n }\n });\n return issues;\n}\n\n// Generates a Data URL based on the current image, then calls \"exported\"\n// with an object including the string, image information, and any issues found\nthis.rasterExport = function (imgType, quality, exportWindowName) {\n const mimeType = 'image/' + imgType.toLowerCase();\n const issues = getIssues();\n const issueCodes = getIssues({codesOnly: true});\n const str = this.svgCanvasToString();\n\n buildCanvgCallback(function () {\n const type = imgType || 'PNG';\n if (!$('#export_canvas').length) {\n $('', {id: 'export_canvas'}).hide().appendTo('body');\n }\n const c = $('#export_canvas')[0];\n c.width = canvas.contentW;\n c.height = canvas.contentH;\n\n canvg(c, str, {renderCallback () {\n const dataURLType = (type === 'ICO' ? 'BMP' : type).toLowerCase();\n const datauri = quality ? c.toDataURL('image/' + dataURLType, quality) : c.toDataURL('image/' + dataURLType);\n if (c.toBlob) {\n c.toBlob(function (blob) {\n const bloburl = createObjectURL(blob);\n call('exported', {datauri, bloburl, svg: str, issues, issueCodes, type: imgType, mimeType, quality, exportWindowName});\n }, mimeType, quality);\n return;\n }\n const bloburl = dataURLToObjectURL(datauri);\n call('exported', {datauri, bloburl, svg: str, issues, issueCodes, type: imgType, mimeType, quality, exportWindowName});\n }});\n })();\n};\n\nthis.exportPDF = function (exportWindowName, outputType) {\n const that = this;\n buildJSPDFCallback(function () {\n const res = getResolution();\n const orientation = res.w > res.h ? 'landscape' : 'portrait';\n const unit = 'pt'; // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for export purposes\n const doc = jsPDF({\n orientation,\n unit,\n format: [res.w, res.h]\n // , compressPdf: true\n }); // Todo: Give options to use predefined jsPDF formats like \"a4\", etc. from pull-down (with option to keep customizable)\n const docTitle = getDocumentTitle();\n doc.setProperties({\n title: docTitle /* ,\n subject: '',\n author: '',\n keywords: '',\n creator: '' */\n });\n const issues = getIssues();\n const issueCodes = getIssues({codesOnly: true});\n const str = that.svgCanvasToString();\n doc.addSVG(str, 0, 0);\n\n // doc.output('save'); // Works to open in a new\n // window; todo: configure this and other export\n // options to optionally work in this manner as\n // opposed to opening a new tab\n const obj = {svg: str, issues, issueCodes, exportWindowName};\n const method = outputType || 'dataurlstring';\n obj[method] = doc.output(method);\n call('exportedPDF', obj);\n })();\n};\n\n/**\n* Returns the current drawing as raw SVG XML text.\n* @returns The current drawing as raw SVG XML text.\n*/\nthis.getSvgString = function () {\n saveOptions.apply = false;\n return this.svgCanvasToString();\n};\n\n/**\n* This function determines whether to use a nonce in the prefix, when\n* generating IDs for future documents in SVG-Edit.\n* @param {Boolean} [enableRandomization] If true, adds a nonce to the prefix. Thus\n* svgCanvas.randomizeIds() <==> svgCanvas.randomizeIds(true)\n*\n* if you're controlling SVG-Edit externally, and want randomized IDs, call\n* this BEFORE calling svgCanvas.setSvgString\n*/\nthis.randomizeIds = function (enableRandomization) {\n if (arguments.length > 0 && enableRandomization === false) {\n draw.randomizeIds(false, getCurrentDrawing());\n } else {\n draw.randomizeIds(true, getCurrentDrawing());\n }\n};\n\n/**\n* Ensure each element has a unique ID\n* @param g - The parent element of the tree to give unique IDs\n*/\nconst uniquifyElems = this.uniquifyElems = function (g) {\n const ids = {};\n // TODO: Handle markers and connectors. These are not yet re-identified properly\n // as their referring elements do not get remapped.\n //\n // \n // \n //\n // Problem #1: if svg_1 gets renamed, we do not update the polyline's se:connector attribute\n // Problem #2: if the polyline svg_7 gets renamed, we do not update the marker id nor the polyline's marker-end attribute\n const refElems = ['filter', 'linearGradient', 'pattern', 'radialGradient', 'symbol', 'textPath', 'use'];\n\n walkTree(g, function (n) {\n // if it's an element node\n if (n.nodeType === 1) {\n // and the element has an ID\n if (n.id) {\n // and we haven't tracked this ID yet\n if (!(n.id in ids)) {\n // add this id to our map\n ids[n.id] = {elem: null, attrs: [], hrefs: []};\n }\n ids[n.id].elem = n;\n }\n\n // now search for all attributes on this element that might refer\n // to other elements\n $.each(refAttrs, function (i, attr) {\n const attrnode = n.getAttributeNode(attr);\n if (attrnode) {\n // the incoming file has been sanitized, so we should be able to safely just strip off the leading #\n const url = getUrlFromAttr(attrnode.value),\n refid = url ? url.substr(1) : null;\n if (refid) {\n if (!(refid in ids)) {\n // add this id to our map\n ids[refid] = {elem: null, attrs: [], hrefs: []};\n }\n ids[refid].attrs.push(attrnode);\n }\n }\n });\n\n // check xlink:href now\n const href = getHref(n);\n // TODO: what if an or element refers to an element internally?\n if (href && refElems.includes(n.nodeName)) {\n const refid = href.substr(1);\n if (refid) {\n if (!(refid in ids)) {\n // add this id to our map\n ids[refid] = {elem: null, attrs: [], hrefs: []};\n }\n ids[refid].hrefs.push(n);\n }\n }\n }\n });\n\n // in ids, we now have a map of ids, elements and attributes, let's re-identify\n for (const oldid in ids) {\n if (!oldid) { continue; }\n const {elem} = ids[oldid];\n if (elem) {\n const newid = getNextId();\n\n // assign element its new id\n elem.id = newid;\n\n // remap all url() attributes\n const {attrs} = ids[oldid];\n let j = attrs.length;\n while (j--) {\n const attr = attrs[j];\n attr.ownerElement.setAttribute(attr.name, 'url(#' + newid + ')');\n }\n\n // remap all href attributes\n const hreffers = ids[oldid].hrefs;\n let k = hreffers.length;\n while (k--) {\n const hreffer = hreffers[k];\n setHref(hreffer, '#' + newid);\n }\n }\n }\n};\n\n/**\n* Assigns reference data for each use element\n*/\nconst setUseData = this.setUseData = function (parent) {\n let elems = $(parent);\n\n if (parent.tagName !== 'use') {\n elems = elems.find('use');\n }\n\n elems.each(function () {\n const id = getHref(this).substr(1);\n const refElem = getElem(id);\n if (!refElem) { return; }\n $(this).data('ref', refElem);\n if (refElem.tagName === 'symbol' || refElem.tagName === 'svg') {\n $(this).data('symbol', refElem).data('ref', refElem);\n }\n });\n};\n\n/**\n* Converts gradients from userSpaceOnUse to objectBoundingBox\n* @param elem\n*/\nconst convertGradients = this.convertGradients = function (elem) {\n let elems = $(elem).find('linearGradient, radialGradient');\n if (!elems.length && isWebkit()) {\n // Bug in webkit prevents regular *Gradient selector search\n elems = $(elem).find('*').filter(function () {\n return (this.tagName.includes('Gradient'));\n });\n }\n\n elems.each(function () {\n const grad = this;\n if ($(grad).attr('gradientUnits') === 'userSpaceOnUse') {\n // TODO: Support more than one element with this ref by duplicating parent grad\n const elems = $(svgcontent).find('[fill=\"url(#' + grad.id + ')\"],[stroke=\"url(#' + grad.id + ')\"]');\n if (!elems.length) { return; }\n\n // get object's bounding box\n const bb = utilsGetBBox(elems[0]);\n\n // This will occur if the element is inside a or a ,\n // in which we shouldn't need to convert anyway.\n if (!bb) { return; }\n\n if (grad.tagName === 'linearGradient') {\n const gCoords = $(grad).attr(['x1', 'y1', 'x2', 'y2']);\n\n // If has transform, convert\n const tlist = grad.gradientTransform.baseVal;\n if (tlist && tlist.numberOfItems > 0) {\n const m = transformListToTransform(tlist).matrix;\n const pt1 = transformPoint(gCoords.x1, gCoords.y1, m);\n const pt2 = transformPoint(gCoords.x2, gCoords.y2, m);\n\n gCoords.x1 = pt1.x;\n gCoords.y1 = pt1.y;\n gCoords.x2 = pt2.x;\n gCoords.y2 = pt2.y;\n grad.removeAttribute('gradientTransform');\n }\n\n $(grad).attr({\n x1: (gCoords.x1 - bb.x) / bb.width,\n y1: (gCoords.y1 - bb.y) / bb.height,\n x2: (gCoords.x2 - bb.x) / bb.width,\n y2: (gCoords.y2 - bb.y) / bb.height\n });\n grad.removeAttribute('gradientUnits');\n }\n // else {\n // Note: radialGradient elements cannot be easily converted\n // because userSpaceOnUse will keep circular gradients, while\n // objectBoundingBox will x/y scale the gradient according to\n // its bbox.\n //\n // For now we'll do nothing, though we should probably have\n // the gradient be updated as the element is moved, as\n // inkscape/illustrator do.\n //\n // const gCoords = $(grad).attr(['cx', 'cy', 'r']);\n //\n // $(grad).attr({\n // cx: (gCoords.cx - bb.x) / bb.width,\n // cy: (gCoords.cy - bb.y) / bb.height,\n // r: gCoords.r\n // });\n //\n // grad.removeAttribute('gradientUnits');\n // }\n }\n });\n};\n\n/**\n* Converts selected/given or child SVG element to a group\n* @param elem\n*/\nconst convertToGroup = this.convertToGroup = function (elem) {\n if (!elem) {\n elem = selectedElements[0];\n }\n const $elem = $(elem);\n const batchCmd = new BatchCommand();\n let ts;\n\n if ($elem.data('gsvg')) {\n // Use the gsvg as the new group\n const svg = elem.firstChild;\n const pt = $(svg).attr(['x', 'y']);\n\n $(elem.firstChild.firstChild).unwrap();\n $(elem).removeData('gsvg');\n\n const tlist = getTransformList(elem);\n const xform = svgroot.createSVGTransform();\n xform.setTranslate(pt.x, pt.y);\n tlist.appendItem(xform);\n recalculateDimensions(elem);\n call('selected', [elem]);\n } else if ($elem.data('symbol')) {\n elem = $elem.data('symbol');\n\n ts = $elem.attr('transform');\n const pos = $elem.attr(['x', 'y']);\n\n const vb = elem.getAttribute('viewBox');\n\n if (vb) {\n const nums = vb.split(' ');\n pos.x -= +nums[0];\n pos.y -= +nums[1];\n }\n\n // Not ideal, but works\n ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')';\n\n const prev = $elem.prev();\n\n // Remove element\n batchCmd.addSubCommand(new RemoveElementCommand($elem[0], $elem[0].nextSibling, $elem[0].parentNode));\n $elem.remove();\n\n // See if other elements reference this symbol\n const hasMore = $(svgcontent).find('use:data(symbol)').length;\n\n const g = svgdoc.createElementNS(NS.SVG, 'g');\n const childs = elem.childNodes;\n\n let i;\n for (i = 0; i < childs.length; i++) {\n g.appendChild(childs[i].cloneNode(true));\n }\n\n // Duplicate the gradients for Gecko, since they weren't included in the \n if (isGecko()) {\n const dupeGrads = $(findDefs()).children('linearGradient,radialGradient,pattern').clone();\n $(g).append(dupeGrads);\n }\n\n if (ts) {\n g.setAttribute('transform', ts);\n }\n\n const parent = elem.parentNode;\n\n uniquifyElems(g);\n\n // Put the dupe gradients back into (after uniquifying them)\n if (isGecko()) {\n $(findDefs()).append($(g).find('linearGradient,radialGradient,pattern'));\n }\n\n // now give the g itself a new id\n g.id = getNextId();\n\n prev.after(g);\n\n if (parent) {\n if (!hasMore) {\n // remove symbol/svg element\n const {nextSibling} = elem;\n parent.removeChild(elem);\n batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));\n }\n batchCmd.addSubCommand(new InsertElementCommand(g));\n }\n\n setUseData(g);\n\n if (isGecko()) {\n convertGradients(findDefs());\n } else {\n convertGradients(g);\n }\n\n // recalculate dimensions on the top-level children so that unnecessary transforms\n // are removed\n walkTreePost(g, function (n) {\n try {\n recalculateDimensions(n);\n } catch (e) {\n console.log(e);\n }\n });\n\n // Give ID for any visible element missing one\n $(g).find(visElems).each(function () {\n if (!this.id) { this.id = getNextId(); }\n });\n\n selectOnly([g]);\n\n const cm = pushGroupProperties(g, true);\n if (cm) {\n batchCmd.addSubCommand(cm);\n }\n\n addCommandToHistory(batchCmd);\n } else {\n console.log('Unexpected element to ungroup:', elem);\n }\n};\n\n/**\n* This function sets the current drawing as the input SVG XML.\n* @param {String} xmlString - The SVG as XML text.\n* @param {Boolean} [preventUndo=false] - Indicates if we want to do the\n* changes without adding them to the undo stack - e.g. for initializing a\n* drawing on page load.\n* @returns {Boolean} This function returns false if the set was\n* unsuccessful, true otherwise.\n*/\nthis.setSvgString = function (xmlString, preventUndo) {\n try {\n // convert string into XML document\n const newDoc = text2xml(xmlString);\n if (newDoc.firstElementChild &&\n newDoc.firstElementChild.namespaceURI !== NS.SVG) {\n return false;\n }\n\n this.prepareSvg(newDoc);\n\n const batchCmd = new BatchCommand('Change Source');\n\n // remove old svg document\n const {nextSibling} = svgcontent;\n const oldzoom = svgroot.removeChild(svgcontent);\n batchCmd.addSubCommand(new RemoveElementCommand(oldzoom, nextSibling, svgroot));\n\n // set new svg document\n // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()\n if (svgdoc.adoptNode) {\n svgcontent = svgdoc.adoptNode(newDoc.documentElement);\n } else {\n svgcontent = svgdoc.importNode(newDoc.documentElement, true);\n }\n\n svgroot.appendChild(svgcontent);\n const content = $(svgcontent);\n\n canvas.current_drawing_ = new draw.Drawing(svgcontent, idprefix);\n\n // retrieve or set the nonce\n const nonce = getCurrentDrawing().getNonce();\n if (nonce) {\n call('setnonce', nonce);\n } else {\n call('unsetnonce');\n }\n\n // change image href vals if possible\n content.find('image').each(function () {\n const image = this;\n preventClickDefault(image);\n const val = getHref(this);\n if (val) {\n if (val.startsWith('data:')) {\n // Check if an SVG-edit data URI\n const m = val.match(/svgedit_url=(.*?);/);\n if (m) {\n const url = decodeURIComponent(m[1]);\n $(new Image()).load(function () {\n image.setAttributeNS(NS.XLINK, 'xlink:href', url);\n }).attr('src', url);\n }\n }\n // Add to encodableImages if it loads\n canvas.embedImage(val);\n }\n });\n\n // Wrap child SVGs in group elements\n content.find('svg').each(function () {\n // Skip if it's in a \n if ($(this).closest('defs').length) { return; }\n\n uniquifyElems(this);\n\n // Check if it already has a gsvg group\n const pa = this.parentNode;\n if (pa.childNodes.length === 1 && pa.nodeName === 'g') {\n $(pa).data('gsvg', this);\n pa.id = pa.id || getNextId();\n } else {\n groupSvgElem(this);\n }\n });\n\n // For Firefox: Put all paint elems in defs\n if (isGecko()) {\n content.find('linearGradient, radialGradient, pattern').appendTo(findDefs());\n }\n\n // Set ref element for elements\n\n // TODO: This should also be done if the object is re-added through \"redo\"\n setUseData(content);\n\n convertGradients(content[0]);\n\n const attrs = {\n id: 'svgcontent',\n overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden'\n };\n\n let percs = false;\n\n // determine proper size\n if (content.attr('viewBox')) {\n const vb = content.attr('viewBox').split(' ');\n attrs.width = vb[2];\n attrs.height = vb[3];\n // handle content that doesn't have a viewBox\n } else {\n $.each(['width', 'height'], function (i, dim) {\n // Set to 100 if not given\n const val = content.attr(dim) || '100%';\n\n if (String(val).substr(-1) === '%') {\n // Use user units if percentage given\n percs = true;\n } else {\n attrs[dim] = convertToNum(dim, val);\n }\n });\n }\n\n // identify layers\n draw.identifyLayers();\n\n // Give ID for any visible layer children missing one\n content.children().find(visElems).each(function () {\n if (!this.id) { this.id = getNextId(); }\n });\n\n // Percentage width/height, so let's base it on visible elements\n if (percs) {\n const bb = getStrokedBBoxDefaultVisible();\n attrs.width = bb.width + bb.x;\n attrs.height = bb.height + bb.y;\n }\n\n // Just in case negative numbers are given or\n // result from the percs calculation\n if (attrs.width <= 0) { attrs.width = 100; }\n if (attrs.height <= 0) { attrs.height = 100; }\n\n content.attr(attrs);\n this.contentW = attrs.width;\n this.contentH = attrs.height;\n\n batchCmd.addSubCommand(new InsertElementCommand(svgcontent));\n // update root to the correct size\n const changes = content.attr(['width', 'height']);\n batchCmd.addSubCommand(new ChangeElementCommand(svgroot, changes));\n\n // reset zoom\n currentZoom = 1;\n\n // reset transform lists\n resetListMap();\n clearSelection();\n pathModule.clearData();\n svgroot.appendChild(selectorManager.selectorParentGroup);\n\n if (!preventUndo) addCommandToHistory(batchCmd);\n call('changed', [svgcontent]);\n } catch (e) {\n console.log(e);\n return false;\n }\n\n return true;\n};\n\n/**\n* This function imports the input SVG XML as a <symbol> in the <defs>, then adds a\n* <use> to the current layer.\n* @param {String} xmlString - The SVG as XML text.\n* @returns This function returns null if the import was unsuccessful, or the element otherwise.\n* @todo\n* - properly handle if namespace is introduced by imported content (must add to svgcontent\n* and update all prefixes in the imported node)\n* - properly handle recalculating dimensions, recalculateDimensions() doesn't handle\n* arbitrary transform lists, but makes some assumptions about how the transform list\n* was obtained\n* - import should happen in top-left of current zoomed viewport\n*/\nthis.importSvgString = function (xmlString) {\n let j, ts, useEl;\n try {\n // Get unique ID\n const uid = encode64(xmlString.length + xmlString).substr(0, 32);\n\n let useExisting = false;\n // Look for symbol and make sure symbol exists in image\n if (importIds[uid]) {\n if ($(importIds[uid].symbol).parents('#svgroot').length) {\n useExisting = true;\n }\n }\n\n const batchCmd = new BatchCommand('Import Image');\n let symbol;\n if (useExisting) {\n ({symbol} = importIds[uid]);\n ts = importIds[uid].xform;\n } else {\n // convert string into XML document\n const newDoc = text2xml(xmlString);\n\n this.prepareSvg(newDoc);\n\n // import new svg document into our document\n let svg;\n // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()\n if (svgdoc.adoptNode) {\n svg = svgdoc.adoptNode(newDoc.documentElement);\n } else {\n svg = svgdoc.importNode(newDoc.documentElement, true);\n }\n\n uniquifyElems(svg);\n\n const innerw = convertToNum('width', svg.getAttribute('width')),\n innerh = convertToNum('height', svg.getAttribute('height')),\n innervb = svg.getAttribute('viewBox'),\n // if no explicit viewbox, create one out of the width and height\n vb = innervb ? innervb.split(' ') : [0, 0, innerw, innerh];\n for (j = 0; j < 4; ++j) {\n vb[j] = +(vb[j]);\n }\n\n // TODO: properly handle preserveAspectRatio\n const // canvasw = +svgcontent.getAttribute('width'),\n canvash = +svgcontent.getAttribute('height');\n // imported content should be 1/3 of the canvas on its largest dimension\n\n if (innerh > innerw) {\n ts = 'scale(' + (canvash / 3) / vb[3] + ')';\n } else {\n ts = 'scale(' + (canvash / 3) / vb[2] + ')';\n }\n\n // Hack to make recalculateDimensions understand how to scale\n ts = 'translate(0) ' + ts + ' translate(0)';\n\n symbol = svgdoc.createElementNS(NS.SVG, 'symbol');\n const defs = findDefs();\n\n if (isGecko()) {\n // Move all gradients into root for Firefox, workaround for this bug:\n // https://bugzilla.mozilla.org/show_bug.cgi?id=353575\n // TODO: Make this properly undo-able.\n $(svg).find('linearGradient, radialGradient, pattern').appendTo(defs);\n }\n\n while (svg.firstChild) {\n const first = svg.firstChild;\n symbol.appendChild(first);\n }\n const attrs = svg.attributes;\n for (let i = 0; i < attrs.length; i++) {\n const attr = attrs[i];\n symbol.setAttribute(attr.nodeName, attr.value);\n }\n symbol.id = getNextId();\n\n // Store data\n importIds[uid] = {\n symbol,\n xform: ts\n };\n\n findDefs().appendChild(symbol);\n batchCmd.addSubCommand(new InsertElementCommand(symbol));\n }\n\n useEl = svgdoc.createElementNS(NS.SVG, 'use');\n useEl.id = getNextId();\n setHref(useEl, '#' + symbol.id);\n\n (currentGroup || getCurrentDrawing().getCurrentLayer()).appendChild(useEl);\n batchCmd.addSubCommand(new InsertElementCommand(useEl));\n clearSelection();\n\n useEl.setAttribute('transform', ts);\n recalculateDimensions(useEl);\n $(useEl).data('symbol', symbol).data('ref', symbol);\n addToSelection([useEl]);\n\n // TODO: Find way to add this in a recalculateDimensions-parsable way\n // if (vb[0] !== 0 || vb[1] !== 0) {\n // ts = 'translate(' + (-vb[0]) + ',' + (-vb[1]) + ') ' + ts;\n // }\n addCommandToHistory(batchCmd);\n call('changed', [svgcontent]);\n } catch (e) {\n console.log(e);\n return null;\n }\n\n // we want to return the element so we can automatically select it\n return useEl;\n};\n\n// Could deprecate, but besides external uses, their usage makes clear that\n// canvas is a dependency for all of these\n[\n 'identifyLayers', 'createLayer', 'cloneLayer', 'deleteCurrentLayer',\n 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition',\n 'setLayerVisibility', 'moveSelectedToLayer', 'mergeLayer', 'mergeAllLayers',\n 'leaveContext', 'setContext'\n].forEach((prop) => {\n canvas[prop] = draw[prop];\n});\ndraw.init({\n pathActions,\n getCurrentGroup () {\n return currentGroup;\n },\n setCurrentGroup (cg) {\n currentGroup = cg;\n },\n getSelectedElements,\n getSVGContent,\n undoMgr,\n elData,\n getCurrentDrawing,\n clearSelection,\n call,\n addCommandToHistory,\n changeSvgcontent () {\n call('changed', [svgcontent]);\n }\n});\n\n/**\n* Group: Document functions\n*/\n\n/**\n* Clears the current document. This is not an undoable action.\n*/\nthis.clear = function () {\n pathActions.clear();\n\n clearSelection();\n\n // clear the svgcontent node\n canvas.clearSvgContentElement();\n\n // create new document\n canvas.current_drawing_ = new draw.Drawing(svgcontent);\n\n // create empty first layer\n canvas.createLayer('Layer 1');\n\n // clear the undo stack\n canvas.undoMgr.resetUndoStack();\n\n // reset the selector manager\n selectorManager.initGroup();\n\n // reset the rubber band box\n rubberBox = selectorManager.getRubberBandBox();\n\n call('cleared');\n};\n\n/**\n* Alias function\n*/\nthis.linkControlPoints = pathActions.linkControlPoints;\n\n/**\n* @returns The content DOM element\n*/\nthis.getContentElem = function () { return svgcontent; };\n\n/**\n* @returns The root DOM element\n*/\nthis.getRootElem = function () { return svgroot; };\n\n/**\n* @returns {Object} The current dimensions and zoom level in an object\n*/\nconst getResolution = this.getResolution = function () {\n// const vb = svgcontent.getAttribute('viewBox').split(' ');\n// return {w:vb[2], h:vb[3], zoom: currentZoom};\n\n const w = svgcontent.getAttribute('width') / currentZoom;\n const h = svgcontent.getAttribute('height') / currentZoom;\n\n return {\n w,\n h,\n zoom: currentZoom\n };\n};\n\n/**\n* @returns The current snap to grid setting\n*/\nthis.getSnapToGrid = function () { return curConfig.gridSnapping; };\n\n/**\n* @returns {String} A string which describes the revision number of SvgCanvas.\n*/\nthis.getVersion = function () {\n return 'svgcanvas.js ($Rev$)';\n};\n\n/**\n* Update interface strings with given values\n* @param strs - Object with strings (see locales file)\n*/\nthis.setUiStrings = function (strs) {\n Object.assign(uiStrings, strs.notification);\n pathModule.setUiStrings(strs);\n};\n\n/**\n* Update configuration options with given values\n* @param {Object} opts - Object with options (see curConfig for examples)\n*/\nthis.setConfig = function (opts) {\n Object.assign(curConfig, opts);\n};\n\n/**\n* @param elem\n* @returns {String|undefined} the current group/SVG's title contents\n*/\nthis.getTitle = function (elem) {\n elem = elem || selectedElements[0];\n if (!elem) { return; }\n elem = $(elem).data('gsvg') || $(elem).data('symbol') || elem;\n const childs = elem.childNodes;\n for (let i = 0; i < childs.length; i++) {\n if (childs[i].nodeName === 'title') {\n return childs[i].textContent;\n }\n }\n return '';\n};\n\n/**\n* Sets the group/SVG's title content\n* @param val\n* @todo Combine this with `setDocumentTitle`\n*/\nthis.setGroupTitle = function (val) {\n let elem = selectedElements[0];\n elem = $(elem).data('gsvg') || elem;\n\n const ts = $(elem).children('title');\n\n const batchCmd = new BatchCommand('Set Label');\n\n let title;\n if (!val.length) {\n // Remove title element\n const tsNextSibling = ts.nextSibling;\n batchCmd.addSubCommand(new RemoveElementCommand(ts[0], tsNextSibling, elem));\n ts.remove();\n } else if (ts.length) {\n // Change title contents\n title = ts[0];\n batchCmd.addSubCommand(new ChangeElementCommand(title, {'#text': title.textContent}));\n title.textContent = val;\n } else {\n // Add title element\n title = svgdoc.createElementNS(NS.SVG, 'title');\n title.textContent = val;\n $(elem).prepend(title);\n batchCmd.addSubCommand(new InsertElementCommand(title));\n }\n\n addCommandToHistory(batchCmd);\n};\n\n/**\n* @returns {String|undefined} The current document title or an empty string if not found\n*/\nconst getDocumentTitle = this.getDocumentTitle = function () {\n return canvas.getTitle(svgcontent);\n};\n\n/**\n* Adds/updates a title element for the document with the given name.\n* This is an undoable action\n* @param {String} newtitle - String with the new title\n*/\nthis.setDocumentTitle = function (newtitle) {\n const childs = svgcontent.childNodes;\n let docTitle = false, oldTitle = '';\n\n const batchCmd = new BatchCommand('Change Image Title');\n\n for (let i = 0; i < childs.length; i++) {\n if (childs[i].nodeName === 'title') {\n docTitle = childs[i];\n oldTitle = docTitle.textContent;\n break;\n }\n }\n if (!docTitle) {\n docTitle = svgdoc.createElementNS(NS.SVG, 'title');\n svgcontent.insertBefore(docTitle, svgcontent.firstChild);\n }\n\n if (newtitle.length) {\n docTitle.textContent = newtitle;\n } else {\n // No title given, so element is not necessary\n docTitle.parentNode.removeChild(docTitle);\n }\n batchCmd.addSubCommand(new ChangeElementCommand(docTitle, {'#text': oldTitle}));\n addCommandToHistory(batchCmd);\n};\n\n/**\n* Returns the editor's namespace URL, optionally adds it to root element\n* @param {Boolean} add - Indicates whether or not to add the namespace value\n* @returns {String} The editor's namespace URL\n*/\nthis.getEditorNS = function (add) {\n if (add) {\n svgcontent.setAttribute('xmlns:se', NS.SE);\n }\n return NS.SE;\n};\n\n/**\n* Changes the document's dimensions to the given size\n* @param x - Number with the width of the new dimensions in user units.\n* Can also be the string \"fit\" to indicate \"fit to content\"\n* @param y - Number with the height of the new dimensions in user units.\n* @returns {Boolean} Indicates if resolution change was succesful.\n* It will fail on \"fit to content\" option with no content to fit to.\n*/\nthis.setResolution = function (x, y) {\n const res = getResolution();\n const {w, h} = res;\n let batchCmd;\n\n if (x === 'fit') {\n // Get bounding box\n const bbox = getStrokedBBoxDefaultVisible();\n\n if (bbox) {\n batchCmd = new BatchCommand('Fit Canvas to Content');\n const visEls = getVisibleElements();\n addToSelection(visEls);\n const dx = [], dy = [];\n $.each(visEls, function (i, item) {\n dx.push(bbox.x * -1);\n dy.push(bbox.y * -1);\n });\n\n const cmd = canvas.moveSelectedElements(dx, dy, true);\n batchCmd.addSubCommand(cmd);\n clearSelection();\n\n x = Math.round(bbox.width);\n y = Math.round(bbox.height);\n } else {\n return false;\n }\n }\n if (x !== w || y !== h) {\n if (!batchCmd) {\n batchCmd = new BatchCommand('Change Image Dimensions');\n }\n\n x = convertToNum('width', x);\n y = convertToNum('height', y);\n\n svgcontent.setAttribute('width', x);\n svgcontent.setAttribute('height', y);\n\n this.contentW = x;\n this.contentH = y;\n batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {width: w, height: h}));\n\n svgcontent.setAttribute('viewBox', [0, 0, x / currentZoom, y / currentZoom].join(' '));\n batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {viewBox: ['0 0', w, h].join(' ')}));\n\n addCommandToHistory(batchCmd);\n call('changed', [svgcontent]);\n }\n return true;\n};\n\n/**\n* @returns An object with x, y values indicating the svgcontent element's\n* position in the editor's canvas.\n*/\nthis.getOffset = function () {\n return $(svgcontent).attr(['x', 'y']);\n};\n\n/**\n* Sets the zoom level on the canvas-side based on the given value\n* @param val - Bounding box object to zoom to or string indicating zoom option\n* @param {Number} editorW - Integer with the editor's workarea box's width\n* @param {Number} editorH - Integer with the editor's workarea box's height\n* @returns {Object|undefined}\n*/\nthis.setBBoxZoom = function (val, editorW, editorH) {\n let spacer = 0.85;\n let bb;\n const calcZoom = function (bb) {\n if (!bb) { return false; }\n const wZoom = Math.round((editorW / bb.width) * 100 * spacer) / 100;\n const hZoom = Math.round((editorH / bb.height) * 100 * spacer) / 100;\n const zoom = Math.min(wZoom, hZoom);\n canvas.setZoom(zoom);\n return {zoom, bbox: bb};\n };\n\n if (typeof val === 'object') {\n bb = val;\n if (bb.width === 0 || bb.height === 0) {\n const newzoom = bb.zoom ? bb.zoom : currentZoom * bb.factor;\n canvas.setZoom(newzoom);\n return {zoom: currentZoom, bbox: bb};\n }\n return calcZoom(bb);\n }\n\n switch (val) {\n case 'selection':\n if (!selectedElements[0]) { return; }\n const selectedElems = $.map(selectedElements, function (n) { if (n) { return n; } });\n bb = getStrokedBBoxDefaultVisible(selectedElems);\n break;\n case 'canvas':\n const res = getResolution();\n spacer = 0.95;\n bb = {width: res.w, height: res.h, x: 0, y: 0};\n break;\n case 'content':\n bb = getStrokedBBoxDefaultVisible();\n break;\n case 'layer':\n bb = getStrokedBBoxDefaultVisible(getVisibleElements(getCurrentDrawing().getCurrentLayer()));\n break;\n default:\n return;\n }\n return calcZoom(bb);\n};\n\n/**\n* Sets the zoom to the given level\n* @param {Number} zoomlevel - Float indicating the zoom level to change to\n*/\nthis.setZoom = function (zoomlevel) {\n const res = getResolution();\n svgcontent.setAttribute('viewBox', '0 0 ' + res.w / zoomlevel + ' ' + res.h / zoomlevel);\n currentZoom = zoomlevel;\n $.each(selectedElements, function (i, elem) {\n if (!elem) { return; }\n selectorManager.requestSelector(elem).resize();\n });\n pathActions.zoomChange();\n runExtensions('zoomChanged', zoomlevel);\n};\n\n/**\n* @returns {String} The current editor mode string\n*/\nthis.getMode = function () {\n return currentMode;\n};\n\n/**\n* Sets the editor's mode to the given string\n* @param {String} name - String with the new mode to change to\n*/\nthis.setMode = function (name) {\n pathActions.clear(true);\n textActions.clear();\n curProperties = (selectedElements[0] && selectedElements[0].nodeName === 'text') ? curText : curShape;\n currentMode = name;\n};\n\n/**\n* Group: Element Styling\n*/\n\n/**\n* @returns The current fill/stroke option\n*/\nthis.getColor = function (type) {\n return curProperties[type];\n};\n\n/**\n* Change the current stroke/fill color/gradient value\n* @param {String} type - String indicating fill or stroke\n* @param val - The value to set the stroke attribute to\n* @param {Boolean} preventUndo - Boolean indicating whether or not this should be and undoable option\n*/\nthis.setColor = function (type, val, preventUndo) {\n curShape[type] = val;\n curProperties[type + '_paint'] = {type: 'solidColor'};\n const elems = [];\n function addNonG (e) {\n if (e.nodeName !== 'g') {\n elems.push(e);\n }\n }\n let i = selectedElements.length;\n while (i--) {\n const elem = selectedElements[i];\n if (elem) {\n if (elem.tagName === 'g') {\n walkTree(elem, addNonG);\n } else {\n if (type === 'fill') {\n if (elem.tagName !== 'polyline' && elem.tagName !== 'line') {\n elems.push(elem);\n }\n } else {\n elems.push(elem);\n }\n }\n }\n }\n if (elems.length > 0) {\n if (!preventUndo) {\n changeSelectedAttribute(type, val, elems);\n call('changed', elems);\n } else {\n changeSelectedAttributeNoUndo(type, val, elems);\n }\n }\n};\n\n// Apply the current gradient to selected element's fill or stroke\n//\n// Parameters\n// type - String indicating \"fill\" or \"stroke\" to apply to an element\nconst setGradient = this.setGradient = function (type) {\n if (!curProperties[type + '_paint'] || curProperties[type + '_paint'].type === 'solidColor') { return; }\n let grad = canvas[type + 'Grad'];\n // find out if there is a duplicate gradient already in the defs\n const duplicateGrad = findDuplicateGradient(grad);\n const defs = findDefs();\n // no duplicate found, so import gradient into defs\n if (!duplicateGrad) {\n // const origGrad = grad;\n grad = defs.appendChild(svgdoc.importNode(grad, true));\n // get next id and set it on the grad\n grad.id = getNextId();\n } else { // use existing gradient\n grad = duplicateGrad;\n }\n canvas.setColor(type, 'url(#' + grad.id + ')');\n};\n\n/**\n* Check if exact gradient already exists\n* @param grad - The gradient DOM element to compare to others\n* @returns The existing gradient if found, null if not\n*/\nconst findDuplicateGradient = function (grad) {\n const defs = findDefs();\n const existingGrads = $(defs).find('linearGradient, radialGradient');\n let i = existingGrads.length;\n const radAttrs = ['r', 'cx', 'cy', 'fx', 'fy'];\n while (i--) {\n const og = existingGrads[i];\n if (grad.tagName === 'linearGradient') {\n if (grad.getAttribute('x1') !== og.getAttribute('x1') ||\n grad.getAttribute('y1') !== og.getAttribute('y1') ||\n grad.getAttribute('x2') !== og.getAttribute('x2') ||\n grad.getAttribute('y2') !== og.getAttribute('y2')\n ) {\n continue;\n }\n } else {\n const gradAttrs = $(grad).attr(radAttrs);\n const ogAttrs = $(og).attr(radAttrs);\n\n let diff = false;\n $.each(radAttrs, function (i, attr) {\n if (gradAttrs[attr] !== ogAttrs[attr]) { diff = true; }\n });\n\n if (diff) { continue; }\n }\n\n // else could be a duplicate, iterate through stops\n const stops = grad.getElementsByTagNameNS(NS.SVG, 'stop');\n const ostops = og.getElementsByTagNameNS(NS.SVG, 'stop');\n\n if (stops.length !== ostops.length) {\n continue;\n }\n\n let j = stops.length;\n while (j--) {\n const stop = stops[j];\n const ostop = ostops[j];\n\n if (stop.getAttribute('offset') !== ostop.getAttribute('offset') ||\n stop.getAttribute('stop-opacity') !== ostop.getAttribute('stop-opacity') ||\n stop.getAttribute('stop-color') !== ostop.getAttribute('stop-color')) {\n break;\n }\n }\n\n if (j === -1) {\n return og;\n }\n } // for each gradient in defs\n\n return null;\n};\n\n/**\n* Set a color/gradient to a fill/stroke\n* @param {\"fill\"|\"stroke\"} type - String with \"fill\" or \"stroke\"\n* @param paint - The jGraduate paint object to apply\n*/\nthis.setPaint = function (type, paint) {\n // make a copy\n const p = new $.jGraduate.Paint(paint);\n this.setPaintOpacity(type, p.alpha / 100, true);\n\n // now set the current paint object\n curProperties[type + '_paint'] = p;\n switch (p.type) {\n case 'solidColor':\n this.setColor(type, p.solidColor !== 'none' ? '#' + p.solidColor : 'none');\n break;\n case 'linearGradient':\n case 'radialGradient':\n canvas[type + 'Grad'] = p[p.type];\n setGradient(type);\n break;\n }\n};\n\n// alias\nthis.setStrokePaint = function (paint) {\n this.setPaint('stroke', paint);\n};\n\nthis.setFillPaint = function (paint) {\n this.setPaint('fill', paint);\n};\n\n/**\n* @returns The current stroke-width value\n*/\nthis.getStrokeWidth = function () {\n return curProperties.stroke_width;\n};\n\n/**\n* Sets the stroke width for the current selected elements\n* When attempting to set a line's width to 0, this changes it to 1 instead\n* @param {Number} val - A Float indicating the new stroke width value\n*/\nthis.setStrokeWidth = function (val) {\n if (val === 0 && ['line', 'path'].includes(currentMode)) {\n canvas.setStrokeWidth(1);\n return;\n }\n curProperties.stroke_width = val;\n\n const elems = [];\n function addNonG (e) {\n if (e.nodeName !== 'g') {\n elems.push(e);\n }\n }\n let i = selectedElements.length;\n while (i--) {\n const elem = selectedElements[i];\n if (elem) {\n if (elem.tagName === 'g') {\n walkTree(elem, addNonG);\n } else {\n elems.push(elem);\n }\n }\n }\n if (elems.length > 0) {\n changeSelectedAttribute('stroke-width', val, elems);\n call('changed', selectedElements);\n }\n};\n\n/**\n* Set the given stroke-related attribute the given value for selected elements\n* @param {String} attr - String with the attribute name\n* @param {String|Number} val - String or number with the attribute value\n*/\nthis.setStrokeAttr = function (attr, val) {\n curShape[attr.replace('-', '_')] = val;\n const elems = [];\n\n let i = selectedElements.length;\n while (i--) {\n const elem = selectedElements[i];\n if (elem) {\n if (elem.tagName === 'g') {\n walkTree(elem, function (e) { if (e.nodeName !== 'g') { elems.push(e); } });\n } else {\n elems.push(elem);\n }\n }\n }\n if (elems.length > 0) {\n changeSelectedAttribute(attr, val, elems);\n call('changed', selectedElements);\n }\n};\n\n/**\n* @returns current style options\n*/\nthis.getStyle = function () {\n return curShape;\n};\n\n/**\n* @returns the current opacity\n*/\nthis.getOpacity = getOpacity;\n\n/**\n* Sets the given opacity to the current selected elements\n* @param val\n*/\nthis.setOpacity = function (val) {\n curShape.opacity = val;\n changeSelectedAttribute('opacity', val);\n};\n\n/**\n* @returns the current fill opacity\n*/\nthis.getFillOpacity = function () {\n return curShape.fill_opacity;\n};\n\n/**\n* @returns the current stroke opacity\n*/\nthis.getStrokeOpacity = function () {\n return curShape.stroke_opacity;\n};\n\n/**\n* Sets the current fill/stroke opacity\n* @param {String} type - String with \"fill\" or \"stroke\"\n* @param {Number} val - Float with the new opacity value\n* @param {Boolean} preventUndo - Indicates whether or not this should be an undoable action\n*/\nthis.setPaintOpacity = function (type, val, preventUndo) {\n curShape[type + '_opacity'] = val;\n if (!preventUndo) {\n changeSelectedAttribute(type + '-opacity', val);\n } else {\n changeSelectedAttributeNoUndo(type + '-opacity', val);\n }\n};\n\n/**\n* Gets the current fill/stroke opacity\n* @param {\"fill\"|\"stroke\"} type - String with \"fill\" or \"stroke\"\n* @returns Fill/stroke opacity\n*/\nthis.getPaintOpacity = function (type) {\n return type === 'fill' ? this.getFillOpacity() : this.getStrokeOpacity();\n};\n\n/**\n* Gets the stdDeviation blur value of the given element\n* @param elem - The element to check the blur value for\n* @returns stdDeviation blur attribute value\n*/\nthis.getBlur = function (elem) {\n let val = 0;\n // const elem = selectedElements[0];\n\n if (elem) {\n const filterUrl = elem.getAttribute('filter');\n if (filterUrl) {\n const blur = getElem(elem.id + '_blur');\n if (blur) {\n val = blur.firstChild.getAttribute('stdDeviation');\n }\n }\n }\n return val;\n};\n\n(function () {\nlet curCommand = null;\nlet filter = null;\nlet filterHidden = false;\n\n/**\n* Sets the stdDeviation blur value on the selected element without being undoable\n* @param val - The new stdDeviation value\n*/\ncanvas.setBlurNoUndo = function (val) {\n if (!filter) {\n canvas.setBlur(val);\n return;\n }\n if (val === 0) {\n // Don't change the StdDev, as that will hide the element.\n // Instead, just remove the value for \"filter\"\n changeSelectedAttributeNoUndo('filter', '');\n filterHidden = true;\n } else {\n const elem = selectedElements[0];\n if (filterHidden) {\n changeSelectedAttributeNoUndo('filter', 'url(#' + elem.id + '_blur)');\n }\n if (isWebkit()) {\n console.log('e', elem);\n elem.removeAttribute('filter');\n elem.setAttribute('filter', 'url(#' + elem.id + '_blur)');\n }\n changeSelectedAttributeNoUndo('stdDeviation', val, [filter.firstChild]);\n canvas.setBlurOffsets(filter, val);\n }\n};\n\nfunction finishChange () {\n const bCmd = canvas.undoMgr.finishUndoableChange();\n curCommand.addSubCommand(bCmd);\n addCommandToHistory(curCommand);\n curCommand = null;\n filter = null;\n}\n\n/**\n* Sets the x, y, with, height values of the filter element in order to\n* make the blur not be clipped. Removes them if not neeeded\n* @param filter - The filter DOM element to update\n* @param stdDev - The standard deviation value on which to base the offset size\n*/\ncanvas.setBlurOffsets = function (filter, stdDev) {\n if (stdDev > 3) {\n // TODO: Create algorithm here where size is based on expected blur\n assignAttributes(filter, {\n x: '-50%',\n y: '-50%',\n width: '200%',\n height: '200%'\n }, 100);\n } else {\n // Removing these attributes hides text in Chrome (see Issue 579)\n if (!isWebkit()) {\n filter.removeAttribute('x');\n filter.removeAttribute('y');\n filter.removeAttribute('width');\n filter.removeAttribute('height');\n }\n }\n};\n\n/**\n* Adds/updates the blur filter to the selected element\n* @param {Number} val - Float with the new stdDeviation blur value\n* @param {Boolean} complete - Boolean indicating whether or not the action should be completed (to add to the undo manager)\n*/\ncanvas.setBlur = function (val, complete) {\n if (curCommand) {\n finishChange();\n return;\n }\n\n // Looks for associated blur, creates one if not found\n const elem = selectedElements[0];\n const elemId = elem.id;\n filter = getElem(elemId + '_blur');\n\n val -= 0;\n\n const batchCmd = new BatchCommand();\n\n // Blur found!\n if (filter) {\n if (val === 0) {\n filter = null;\n }\n } else {\n // Not found, so create\n const newblur = addSvgElementFromJson({ element: 'feGaussianBlur',\n attr: {\n in: 'SourceGraphic',\n stdDeviation: val\n }\n });\n\n filter = addSvgElementFromJson({ element: 'filter',\n attr: {\n id: elemId + '_blur'\n }\n });\n\n filter.appendChild(newblur);\n findDefs().appendChild(filter);\n\n batchCmd.addSubCommand(new InsertElementCommand(filter));\n }\n\n const changes = {filter: elem.getAttribute('filter')};\n\n if (val === 0) {\n elem.removeAttribute('filter');\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));\n return;\n }\n\n changeSelectedAttribute('filter', 'url(#' + elemId + '_blur)');\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));\n canvas.setBlurOffsets(filter, val);\n\n curCommand = batchCmd;\n canvas.undoMgr.beginUndoableChange('stdDeviation', [filter ? filter.firstChild : null]);\n if (complete) {\n canvas.setBlurNoUndo(val);\n finishChange();\n }\n};\n}());\n\n/**\n* Check whether selected element is bold or not\n* @returns {Boolean} Indicates whether or not element is bold\n*/\nthis.getBold = function () {\n // should only have one element selected\n const selected = selectedElements[0];\n if (selected != null && selected.tagName === 'text' &&\n selectedElements[1] == null) {\n return (selected.getAttribute('font-weight') === 'bold');\n }\n return false;\n};\n\n/**\n* Make the selected element bold or normal\n* @param {Boolean} b - Indicates bold (true) or normal (false)\n*/\nthis.setBold = function (b) {\n const selected = selectedElements[0];\n if (selected != null && selected.tagName === 'text' &&\n selectedElements[1] == null) {\n changeSelectedAttribute('font-weight', b ? 'bold' : 'normal');\n }\n if (!selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* Check whether selected element is italic or not\n* @returns {Boolean} Indicates whether or not element is italic\n*/\nthis.getItalic = function () {\n const selected = selectedElements[0];\n if (selected != null && selected.tagName === 'text' &&\n selectedElements[1] == null) {\n return (selected.getAttribute('font-style') === 'italic');\n }\n return false;\n};\n\n/**\n* Make the selected element italic or normal\n* @param {Boolean} b - Indicates italic (true) or normal (false)\n*/\nthis.setItalic = function (i) {\n const selected = selectedElements[0];\n if (selected != null && selected.tagName === 'text' &&\n selectedElements[1] == null) {\n changeSelectedAttribute('font-style', i ? 'italic' : 'normal');\n }\n if (!selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* @returns The current font family\n*/\nthis.getFontFamily = function () {\n return curText.font_family;\n};\n\n/**\n* Set the new font family\n* @param {String} val - String with the new font family\n*/\nthis.setFontFamily = function (val) {\n curText.font_family = val;\n changeSelectedAttribute('font-family', val);\n if (selectedElements[0] && !selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* Set the new font color\n* @param {String} val - String with the new font color\n*/\nthis.setFontColor = function (val) {\n curText.fill = val;\n changeSelectedAttribute('fill', val);\n};\n\n/**\n* @returns The current font color\n*/\nthis.getFontColor = function () {\n return curText.fill;\n};\n\n/**\n* Returns the current font size\n*/\nthis.getFontSize = function () {\n return curText.font_size;\n};\n\n/**\n* Applies the given font size to the selected element\n* @param {Number} val - Float with the new font size\n*/\nthis.setFontSize = function (val) {\n curText.font_size = val;\n changeSelectedAttribute('font-size', val);\n if (!selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* @returns The current text (textContent) of the selected element\n*/\nthis.getText = function () {\n const selected = selectedElements[0];\n if (selected == null) { return ''; }\n return selected.textContent;\n};\n\n/**\n* Updates the text element with the given string\n* @param {String} val - String with the new text\n*/\nthis.setTextContent = function (val) {\n changeSelectedAttribute('#text', val);\n textActions.init(val);\n textActions.setCursor();\n};\n\n/**\n* Sets the new image URL for the selected image element. Updates its size if\n* a new URL is given\n* @param {String} val - String with the image URL/path\n*/\nthis.setImageURL = function (val) {\n const elem = selectedElements[0];\n if (!elem) { return; }\n\n const attrs = $(elem).attr(['width', 'height']);\n let setsize = (!attrs.width || !attrs.height);\n\n const curHref = getHref(elem);\n\n // Do nothing if no URL change or size change\n if (curHref !== val) {\n setsize = true;\n } else if (!setsize) { return; }\n\n const batchCmd = new BatchCommand('Change Image URL');\n\n setHref(elem, val);\n batchCmd.addSubCommand(new ChangeElementCommand(elem, {\n '#href': curHref\n }));\n\n if (setsize) {\n $(new Image()).load(function () {\n const changes = $(elem).attr(['width', 'height']);\n\n $(elem).attr({\n width: this.width,\n height: this.height\n });\n\n selectorManager.requestSelector(elem).resize();\n\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));\n addCommandToHistory(batchCmd);\n call('changed', [elem]);\n }).attr('src', val);\n } else {\n addCommandToHistory(batchCmd);\n }\n};\n\n/**\n* Sets the new link URL for the selected anchor element.\n* @param {String} val - String with the link URL/path\n*/\nthis.setLinkURL = function (val) {\n let elem = selectedElements[0];\n if (!elem) { return; }\n if (elem.tagName !== 'a') {\n // See if parent is an anchor\n const parentsA = $(elem).parents('a');\n if (parentsA.length) {\n elem = parentsA[0];\n } else {\n return;\n }\n }\n\n const curHref = getHref(elem);\n\n if (curHref === val) { return; }\n\n const batchCmd = new BatchCommand('Change Link URL');\n\n setHref(elem, val);\n batchCmd.addSubCommand(new ChangeElementCommand(elem, {\n '#href': curHref\n }));\n\n addCommandToHistory(batchCmd);\n};\n\n/**\n* Sets the rx & ry values to the selected rect element to change its corner radius\n* @param val - The new radius\n*/\nthis.setRectRadius = function (val) {\n const selected = selectedElements[0];\n if (selected != null && selected.tagName === 'rect') {\n const r = selected.getAttribute('rx');\n if (r !== String(val)) {\n selected.setAttribute('rx', val);\n selected.setAttribute('ry', val);\n addCommandToHistory(new ChangeElementCommand(selected, {rx: r, ry: r}, 'Radius'));\n call('changed', [selected]);\n }\n }\n};\n\n/**\n* Wraps the selected element(s) in an anchor element or converts group to one\n* @param url\n*/\nthis.makeHyperlink = function (url) {\n canvas.groupSelectedElements('a', url);\n\n // TODO: If element is a single \"g\", convert to \"a\"\n // if (selectedElements.length > 1 && selectedElements[1]) {\n};\n\n/**\n*\n*/\nthis.removeHyperlink = function () {\n canvas.ungroupSelectedElement();\n};\n\n/**\n* Group: Element manipulation\n*/\n\n/**\n* Sets the new segment type to the selected segment(s).\n* @param {Number} newType - Integer with the new segment type\n* See https://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg for list\n*/\nthis.setSegType = function (newType) {\n pathActions.setSegType(newType);\n};\n\n/**\n* @todo (codedread): Remove the getBBox argument and split this function into two.\n* Convert selected element to a path, or get the BBox of an element-as-path\n* @param elem - The DOM element to be converted\n* @param getBBox - Boolean on whether or not to only return the path's BBox\n* @returns If the getBBox flag is true, the resulting path's bounding box object.\n* Otherwise the resulting path element is returned.\n*/\nthis.convertToPath = function (elem, getBBox) {\n if (elem == null) {\n const elems = selectedElements;\n $.each(elems, function (i, elem) {\n if (elem) { canvas.convertToPath(elem); }\n });\n return;\n }\n if (getBBox) {\n return getBBoxOfElementAsPath(elem, addSvgElementFromJson, pathActions);\n } else {\n // TODO: Why is this applying attributes from curShape, then inside utilities.convertToPath it's pulling addition attributes from elem?\n // TODO: If convertToPath is called with one elem, curShape and elem are probably the same; but calling with multiple is a bug or cool feature.\n const attrs = {\n fill: curShape.fill,\n 'fill-opacity': curShape.fill_opacity,\n stroke: curShape.stroke,\n 'stroke-width': curShape.stroke_width,\n 'stroke-dasharray': curShape.stroke_dasharray,\n 'stroke-linejoin': curShape.stroke_linejoin,\n 'stroke-linecap': curShape.stroke_linecap,\n 'stroke-opacity': curShape.stroke_opacity,\n opacity: curShape.opacity,\n visibility: 'hidden'\n };\n return convertToPath(elem, attrs, addSvgElementFromJson, pathActions, clearSelection, addToSelection, history, addCommandToHistory);\n }\n};\n\n/**\n* This function makes the changes to the elements. It does not add the change\n* to the history stack.\n* @param {String} attr - Attribute name\n* @param {String|Number} newValue - String or number with the new attribute value\n* @param elems - The DOM elements to apply the change to\n*/\nconst changeSelectedAttributeNoUndo = function (attr, newValue, elems) {\n if (currentMode === 'pathedit') {\n // Editing node\n pathActions.moveNode(attr, newValue);\n }\n elems = elems || selectedElements;\n let i = elems.length;\n const noXYElems = ['g', 'polyline', 'path'];\n const goodGAttrs = ['transform', 'opacity', 'filter'];\n\n while (i--) {\n let elem = elems[i];\n if (elem == null) { continue; }\n\n // Set x,y vals on elements that don't have them\n if ((attr === 'x' || attr === 'y') && noXYElems.includes(elem.tagName)) {\n const bbox = getStrokedBBoxDefaultVisible([elem]);\n const diffX = attr === 'x' ? newValue - bbox.x : 0;\n const diffY = attr === 'y' ? newValue - bbox.y : 0;\n canvas.moveSelectedElements(diffX * currentZoom, diffY * currentZoom, true);\n continue;\n }\n\n // only allow the transform/opacity/filter attribute to change on elements, slightly hacky\n // TODO: FIXME: This doesn't seem right. Where's the body of this if statement?\n if (elem.tagName === 'g' && goodGAttrs.includes(attr)) {}\n let oldval = attr === '#text' ? elem.textContent : elem.getAttribute(attr);\n if (oldval == null) { oldval = ''; }\n if (oldval !== String(newValue)) {\n if (attr === '#text') {\n // const oldW = utilsGetBBox(elem).width;\n elem.textContent = newValue;\n\n // FF bug occurs on on rotated elements\n if ((/rotate/).test(elem.getAttribute('transform'))) {\n elem = ffClone(elem);\n }\n // Hoped to solve the issue of moving text with text-anchor=\"start\",\n // but this doesn't actually fix it. Hopefully on the right track, though. -Fyrd\n // const box = getBBox(elem), left = box.x, top = box.y, {width, height} = box,\n // dx = width - oldW, dy = 0;\n // const angle = getRotationAngle(elem, true);\n // if (angle) {\n // const r = Math.sqrt(dx * dx + dy * dy);\n // const theta = Math.atan2(dy, dx) - angle;\n // dx = r * Math.cos(theta);\n // dy = r * Math.sin(theta);\n //\n // elem.setAttribute('x', elem.getAttribute('x') - dx);\n // elem.setAttribute('y', elem.getAttribute('y') - dy);\n // }\n } else if (attr === '#href') {\n setHref(elem, newValue);\n } else { elem.setAttribute(attr, newValue); }\n\n // Go into \"select\" mode for text changes\n // NOTE: Important that this happens AFTER elem.setAttribute() or else attributes like\n // font-size can get reset to their old value, ultimately by svgEditor.updateContextPanel(),\n // after calling textActions.toSelectMode() below\n if (currentMode === 'textedit' && attr !== '#text' && elem.textContent.length) {\n textActions.toSelectMode(elem);\n }\n\n // if (i === 0) {\n // selectedBBoxes[0] = utilsGetBBox(elem);\n // }\n\n // Use the Firefox ffClone hack for text elements with gradients or\n // where other text attributes are changed.\n if (isGecko() && elem.nodeName === 'text' && (/rotate/).test(elem.getAttribute('transform'))) {\n if (String(newValue).startsWith('url') || (['font-size', 'font-family', 'x', 'y'].includes(attr) && elem.textContent)) {\n elem = ffClone(elem);\n }\n }\n // Timeout needed for Opera & Firefox\n // codedread: it is now possible for this function to be called with elements\n // that are not in the selectedElements array, we need to only request a\n // selector if the element is in that array\n if (selectedElements.includes(elem)) {\n setTimeout(function () {\n // Due to element replacement, this element may no longer\n // be part of the DOM\n if (!elem.parentNode) { return; }\n selectorManager.requestSelector(elem).resize();\n }, 0);\n }\n // if this element was rotated, and we changed the position of this element\n // we need to update the rotational transform attribute\n const angle = getRotationAngle(elem);\n if (angle !== 0 && attr !== 'transform') {\n const tlist = getTransformList(elem);\n let n = tlist.numberOfItems;\n while (n--) {\n const xform = tlist.getItem(n);\n if (xform.type === 4) {\n // remove old rotate\n tlist.removeItem(n);\n\n const box = utilsGetBBox(elem);\n const center = transformPoint(box.x + box.width / 2, box.y + box.height / 2, transformListToTransform(tlist).matrix);\n const cx = center.x,\n cy = center.y;\n const newrot = svgroot.createSVGTransform();\n newrot.setRotate(angle, cx, cy);\n tlist.insertItemBefore(newrot, n);\n break;\n }\n }\n }\n } // if oldValue != newValue\n } // for each elem\n};\n\n/**\n* Change the given/selected element and add the original value to the history stack\n* If you want to change all selectedElements, ignore the elems argument.\n* If you want to change only a subset of selectedElements, then send the\n* subset to this function in the elems argument.\n* @param {String} attr - String with the attribute name\n* @param {String|Number} newValue - String or number with the new attribute value\n* @param elems - The DOM elements to apply the change to\n*/\nconst changeSelectedAttribute = this.changeSelectedAttribute = function (attr, val, elems) {\n elems = elems || selectedElements;\n canvas.undoMgr.beginUndoableChange(attr, elems);\n // const i = elems.length;\n\n changeSelectedAttributeNoUndo(attr, val, elems);\n\n const batchCmd = canvas.undoMgr.finishUndoableChange();\n if (!batchCmd.isEmpty()) {\n addCommandToHistory(batchCmd);\n }\n};\n\n// Removes all selected elements from the DOM and adds the change to the\n// history stack\nthis.deleteSelectedElements = function () {\n const batchCmd = new BatchCommand('Delete Elements');\n const len = selectedElements.length;\n const selectedCopy = []; // selectedElements is being deleted\n\n for (let i = 0; i < len; ++i) {\n const selected = selectedElements[i];\n if (selected == null) { break; }\n\n let parent = selected.parentNode;\n let t = selected;\n\n // this will unselect the element and remove the selectedOutline\n selectorManager.releaseSelector(t);\n\n // Remove the path if present.\n pathModule.removePath_(t.id);\n\n // Get the parent if it's a single-child anchor\n if (parent.tagName === 'a' && parent.childNodes.length === 1) {\n t = parent;\n parent = parent.parentNode;\n }\n\n const {nextSibling} = t;\n const elem = parent.removeChild(t);\n selectedCopy.push(selected); // for the copy\n batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));\n }\n selectedElements = [];\n\n if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); }\n call('changed', selectedCopy);\n clearSelection();\n};\n\n/**\n* Removes all selected elements from the DOM and adds the change to the\n* history stack. Remembers removed elements on the clipboard\n*/\nthis.cutSelectedElements = function () {\n canvas.copySelectedElements();\n canvas.deleteSelectedElements();\n};\n\n/**\n* Remembers the current selected elements on the clipboard\n*/\nthis.copySelectedElements = function () {\n localStorage.setItem('svgedit_clipboard', JSON.stringify(\n selectedElements.map(function (x) { return getJsonFromSvgElement(x); })\n ));\n\n $('#cmenu_canvas').enableContextMenuItems('#paste,#paste_in_place');\n};\n\nthis.pasteElements = function (type, x, y) {\n let cb = JSON.parse(localStorage.getItem('svgedit_clipboard'));\n let len = cb.length;\n if (!len) { return; }\n\n const pasted = [];\n const batchCmd = new BatchCommand('Paste elements');\n // const drawing = getCurrentDrawing();\n const changedIDs = {};\n\n // Recursively replace IDs and record the changes\n function checkIDs (elem) {\n if (elem.attr && elem.attr.id) {\n changedIDs[elem.attr.id] = getNextId();\n elem.attr.id = changedIDs[elem.attr.id];\n }\n if (elem.children) elem.children.forEach(checkIDs);\n }\n cb.forEach(checkIDs);\n\n // Give extensions like the connector extension a chance to reflect new IDs and remove invalid elements\n runExtensions('IDsUpdated', {elems: cb, changes: changedIDs}, true).forEach(function (extChanges) {\n if (!extChanges || !('remove' in extChanges)) return;\n\n extChanges.remove.forEach(function (removeID) {\n cb = cb.filter(function (cbItem) {\n return cbItem.attr.id !== removeID;\n });\n });\n });\n\n // Move elements to lastClickPoint\n while (len--) {\n const elem = cb[len];\n if (!elem) { continue; }\n\n const copy = addSvgElementFromJson(elem);\n pasted.push(copy);\n batchCmd.addSubCommand(new InsertElementCommand(copy));\n\n restoreRefElems(copy);\n }\n\n selectOnly(pasted);\n\n if (type !== 'in_place') {\n let ctrX, ctrY;\n\n if (!type) {\n ctrX = lastClickPoint.x;\n ctrY = lastClickPoint.y;\n } else if (type === 'point') {\n ctrX = x;\n ctrY = y;\n }\n\n const bbox = getStrokedBBoxDefaultVisible(pasted);\n const cx = ctrX - (bbox.x + bbox.width / 2),\n cy = ctrY - (bbox.y + bbox.height / 2),\n dx = [],\n dy = [];\n\n $.each(pasted, function (i, item) {\n dx.push(cx);\n dy.push(cy);\n });\n\n const cmd = canvas.moveSelectedElements(dx, dy, false);\n if (cmd) batchCmd.addSubCommand(cmd);\n }\n\n addCommandToHistory(batchCmd);\n call('changed', pasted);\n};\n\n/**\n* Wraps all the selected elements in a group (g) element\n* @param type - type of element to group into, defaults to <g>\n*/\nthis.groupSelectedElements = function (type, urlArg) {\n if (!type) { type = 'g'; }\n let cmdStr = '';\n let url;\n\n switch (type) {\n case 'a': {\n cmdStr = 'Make hyperlink';\n url = '';\n if (arguments.length > 1) {\n url = urlArg;\n }\n break;\n } default: {\n type = 'g';\n cmdStr = 'Group Elements';\n break;\n }\n }\n\n const batchCmd = new BatchCommand(cmdStr);\n\n // create and insert the group element\n const g = addSvgElementFromJson({\n element: type,\n attr: {\n id: getNextId()\n }\n });\n if (type === 'a') {\n setHref(g, url);\n }\n batchCmd.addSubCommand(new InsertElementCommand(g));\n\n // now move all children into the group\n let i = selectedElements.length;\n while (i--) {\n let elem = selectedElements[i];\n if (elem == null) { continue; }\n\n if (elem.parentNode.tagName === 'a' && elem.parentNode.childNodes.length === 1) {\n elem = elem.parentNode;\n }\n\n const oldNextSibling = elem.nextSibling;\n const oldParent = elem.parentNode;\n g.appendChild(elem);\n batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));\n }\n if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); }\n\n // update selection\n selectOnly([g], true);\n};\n\n// Pushes all appropriate parent group properties down to its children, then\n// removes them from the group\nconst pushGroupProperties = this.pushGroupProperties = function (g, undoable) {\n const children = g.childNodes;\n const len = children.length;\n const xform = g.getAttribute('transform');\n\n const glist = getTransformList(g);\n const m = transformListToTransform(glist).matrix;\n\n const batchCmd = new BatchCommand('Push group properties');\n\n // TODO: get all fill/stroke properties from the group that we are about to destroy\n // \"fill\", \"fill-opacity\", \"fill-rule\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\",\n // \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\",\n // \"stroke-width\"\n // and then for each child, if they do not have the attribute (or the value is 'inherit')\n // then set the child's attribute\n\n const gangle = getRotationAngle(g);\n\n const gattrs = $(g).attr(['filter', 'opacity']);\n let gfilter, gblur, changes;\n const drawing = getCurrentDrawing();\n\n for (let i = 0; i < len; i++) {\n const elem = children[i];\n\n if (elem.nodeType !== 1) { continue; }\n\n if (gattrs.opacity !== null && gattrs.opacity !== 1) {\n // const c_opac = elem.getAttribute('opacity') || 1;\n const newOpac = Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100) / 100;\n changeSelectedAttribute('opacity', newOpac, [elem]);\n }\n\n if (gattrs.filter) {\n let cblur = this.getBlur(elem);\n const origCblur = cblur;\n if (!gblur) { gblur = this.getBlur(g); }\n if (cblur) {\n // Is this formula correct?\n cblur = Number(gblur) + Number(cblur);\n } else if (cblur === 0) {\n cblur = gblur;\n }\n\n // If child has no current filter, get group's filter or clone it.\n if (!origCblur) {\n // Set group's filter to use first child's ID\n if (!gfilter) {\n gfilter = getRefElem(gattrs.filter);\n } else {\n // Clone the group's filter\n gfilter = drawing.copyElem(gfilter);\n findDefs().appendChild(gfilter);\n }\n } else {\n gfilter = getRefElem(elem.getAttribute('filter'));\n }\n\n // Change this in future for different filters\n const suffix = (gfilter.firstChild.tagName === 'feGaussianBlur') ? 'blur' : 'filter';\n gfilter.id = elem.id + '_' + suffix;\n changeSelectedAttribute('filter', 'url(#' + gfilter.id + ')', [elem]);\n\n // Update blur value\n if (cblur) {\n changeSelectedAttribute('stdDeviation', cblur, [gfilter.firstChild]);\n canvas.setBlurOffsets(gfilter, cblur);\n }\n }\n\n let chtlist = getTransformList(elem);\n\n // Don't process gradient transforms\n if (elem.tagName.includes('Gradient')) { chtlist = null; }\n\n // Hopefully not a problem to add this. Necessary for elements like \n if (!chtlist) { continue; }\n\n // Apparently can get get a transformlist, but we don't want it to have one!\n if (elem.tagName === 'defs') { continue; }\n\n if (glist.numberOfItems) {\n // TODO: if the group's transform is just a rotate, we can always transfer the\n // rotate() down to the children (collapsing consecutive rotates and factoring\n // out any translates)\n if (gangle && glist.numberOfItems === 1) {\n // [Rg] [Rc] [Mc]\n // we want [Tr] [Rc2] [Mc] where:\n // - [Rc2] is at the child's current center but has the\n // sum of the group and child's rotation angles\n // - [Tr] is the equivalent translation that this child\n // undergoes if the group wasn't there\n\n // [Tr] = [Rg] [Rc] [Rc2_inv]\n\n // get group's rotation matrix (Rg)\n const rgm = glist.getItem(0).matrix;\n\n // get child's rotation matrix (Rc)\n let rcm = svgroot.createSVGMatrix();\n const cangle = getRotationAngle(elem);\n if (cangle) {\n rcm = chtlist.getItem(0).matrix;\n }\n\n // get child's old center of rotation\n const cbox = utilsGetBBox(elem);\n const ceqm = transformListToTransform(chtlist).matrix;\n const coldc = transformPoint(cbox.x + cbox.width / 2, cbox.y + cbox.height / 2, ceqm);\n\n // sum group and child's angles\n const sangle = gangle + cangle;\n\n // get child's rotation at the old center (Rc2_inv)\n const r2 = svgroot.createSVGTransform();\n r2.setRotate(sangle, coldc.x, coldc.y);\n\n // calculate equivalent translate\n const trm = matrixMultiply(rgm, rcm, r2.matrix.inverse());\n\n // set up tlist\n if (cangle) {\n chtlist.removeItem(0);\n }\n\n if (sangle) {\n if (chtlist.numberOfItems) {\n chtlist.insertItemBefore(r2, 0);\n } else {\n chtlist.appendItem(r2);\n }\n }\n\n if (trm.e || trm.f) {\n const tr = svgroot.createSVGTransform();\n tr.setTranslate(trm.e, trm.f);\n if (chtlist.numberOfItems) {\n chtlist.insertItemBefore(tr, 0);\n } else {\n chtlist.appendItem(tr);\n }\n }\n } else { // more complicated than just a rotate\n // transfer the group's transform down to each child and then\n // call recalculateDimensions()\n const oldxform = elem.getAttribute('transform');\n changes = {};\n changes.transform = oldxform || '';\n\n const newxform = svgroot.createSVGTransform();\n\n // [ gm ] [ chm ] = [ chm ] [ gm' ]\n // [ gm' ] = [ chmInv ] [ gm ] [ chm ]\n const chm = transformListToTransform(chtlist).matrix,\n chmInv = chm.inverse();\n const gm = matrixMultiply(chmInv, m, chm);\n newxform.setMatrix(gm);\n chtlist.appendItem(newxform);\n }\n const cmd = recalculateDimensions(elem);\n if (cmd) { batchCmd.addSubCommand(cmd); }\n }\n }\n\n // remove transform and make it undo-able\n if (xform) {\n changes = {};\n changes.transform = xform;\n g.setAttribute('transform', '');\n g.removeAttribute('transform');\n batchCmd.addSubCommand(new ChangeElementCommand(g, changes));\n }\n\n if (undoable && !batchCmd.isEmpty()) {\n return batchCmd;\n }\n};\n\n/**\n* Unwraps all the elements in a selected group (g) element. This requires\n* significant recalculations to apply group's transforms, etc to its children\n*/\nthis.ungroupSelectedElement = function () {\n let g = selectedElements[0];\n if (!g) {\n return;\n }\n if ($(g).data('gsvg') || $(g).data('symbol')) {\n // Is svg, so actually convert to group\n convertToGroup(g);\n return;\n }\n if (g.tagName === 'use') {\n // Somehow doesn't have data set, so retrieve\n const symbol = getElem(getHref(g).substr(1));\n $(g).data('symbol', symbol).data('ref', symbol);\n convertToGroup(g);\n return;\n }\n const parentsA = $(g).parents('a');\n if (parentsA.length) {\n g = parentsA[0];\n }\n\n // Look for parent \"a\"\n if (g.tagName === 'g' || g.tagName === 'a') {\n const batchCmd = new BatchCommand('Ungroup Elements');\n const cmd = pushGroupProperties(g, true);\n if (cmd) { batchCmd.addSubCommand(cmd); }\n\n const parent = g.parentNode;\n const anchor = g.nextSibling;\n const children = new Array(g.childNodes.length);\n\n let i = 0;\n while (g.firstChild) {\n let elem = g.firstChild;\n const oldNextSibling = elem.nextSibling;\n const oldParent = elem.parentNode;\n\n // Remove child title elements\n if (elem.tagName === 'title') {\n const {nextSibling} = elem;\n batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, oldParent));\n oldParent.removeChild(elem);\n continue;\n }\n\n children[i++] = elem = parent.insertBefore(elem, anchor);\n batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));\n }\n\n // remove the group from the selection\n clearSelection();\n\n // delete the group element (but make undo-able)\n const gNextSibling = g.nextSibling;\n g = parent.removeChild(g);\n batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent));\n\n if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); }\n\n // update selection\n addToSelection(children);\n }\n};\n\n/**\n* Repositions the selected element to the bottom in the DOM to appear on top of\n* other elements\n*/\nthis.moveToTopSelectedElement = function () {\n const selected = selectedElements[0];\n if (selected != null) {\n let t = selected;\n const oldParent = t.parentNode;\n const oldNextSibling = t.nextSibling;\n t = t.parentNode.appendChild(t);\n // If the element actually moved position, add the command and fire the changed\n // event handler.\n if (oldNextSibling !== t.nextSibling) {\n addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'top'));\n call('changed', [t]);\n }\n }\n};\n\n/**\n* Repositions the selected element to the top in the DOM to appear under\n* other elements\n*/\nthis.moveToBottomSelectedElement = function () {\n const selected = selectedElements[0];\n if (selected != null) {\n let t = selected;\n const oldParent = t.parentNode;\n const oldNextSibling = t.nextSibling;\n let {firstChild} = t.parentNode;\n if (firstChild.tagName === 'title') {\n firstChild = firstChild.nextSibling;\n }\n // This can probably be removed, as the defs should not ever apppear\n // inside a layer group\n if (firstChild.tagName === 'defs') {\n firstChild = firstChild.nextSibling;\n }\n t = t.parentNode.insertBefore(t, firstChild);\n // If the element actually moved position, add the command and fire the changed\n // event handler.\n if (oldNextSibling !== t.nextSibling) {\n addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'bottom'));\n call('changed', [t]);\n }\n }\n};\n\n/**\n* Moves the select element up or down the stack, based on the visibly\n* intersecting elements\n* @param {\"Up\"|\"Down\"} dir - String that's either 'Up' or 'Down'\n*/\nthis.moveUpDownSelected = function (dir) {\n const selected = selectedElements[0];\n if (!selected) { return; }\n\n curBBoxes = [];\n let closest, foundCur;\n // jQuery sorts this list\n const list = $(getIntersectionList(getStrokedBBoxDefaultVisible([selected]))).toArray();\n if (dir === 'Down') { list.reverse(); }\n\n $.each(list, function () {\n if (!foundCur) {\n if (this === selected) {\n foundCur = true;\n }\n return;\n }\n closest = this;\n return false;\n });\n if (!closest) { return; }\n\n const t = selected;\n const oldParent = t.parentNode;\n const oldNextSibling = t.nextSibling;\n $(closest)[dir === 'Down' ? 'before' : 'after'](t);\n // If the element actually moved position, add the command and fire the changed\n // event handler.\n if (oldNextSibling !== t.nextSibling) {\n addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir));\n call('changed', [t]);\n }\n};\n\n/**\n* Moves selected elements on the X/Y axis\n* @param {Number} dx - Float with the distance to move on the x-axis\n* @param {Number} dy - Float with the distance to move on the y-axis\n* @param {Boolean} undoable - Boolean indicating whether or not the action should be undoable\n* @returns Batch command for the move\n*/\nthis.moveSelectedElements = function (dx, dy, undoable) {\n // if undoable is not sent, default to true\n // if single values, scale them to the zoom\n if (dx.constructor !== Array) {\n dx /= currentZoom;\n dy /= currentZoom;\n }\n undoable = undoable || true;\n const batchCmd = new BatchCommand('position');\n let i = selectedElements.length;\n while (i--) {\n const selected = selectedElements[i];\n if (selected != null) {\n // if (i === 0) {\n // selectedBBoxes[0] = utilsGetBBox(selected);\n // }\n // const b = {};\n // for (const j in selectedBBoxes[i]) b[j] = selectedBBoxes[i][j];\n // selectedBBoxes[i] = b;\n\n const xform = svgroot.createSVGTransform();\n const tlist = getTransformList(selected);\n\n // dx and dy could be arrays\n if (dx.constructor === Array) {\n // if (i === 0) {\n // selectedBBoxes[0].x += dx[0];\n // selectedBBoxes[0].y += dy[0];\n // }\n xform.setTranslate(dx[i], dy[i]);\n } else {\n // if (i === 0) {\n // selectedBBoxes[0].x += dx;\n // selectedBBoxes[0].y += dy;\n // }\n xform.setTranslate(dx, dy);\n }\n\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(xform, 0);\n } else {\n tlist.appendItem(xform);\n }\n\n const cmd = recalculateDimensions(selected);\n if (cmd) {\n batchCmd.addSubCommand(cmd);\n }\n\n selectorManager.requestSelector(selected).resize();\n }\n }\n if (!batchCmd.isEmpty()) {\n if (undoable) {\n addCommandToHistory(batchCmd);\n }\n call('changed', selectedElements);\n return batchCmd;\n }\n};\n\n/**\n* Create deep DOM copies (clones) of all selected elements and move them slightly\n* from their originals\n*/\nthis.cloneSelectedElements = function (x, y) {\n let i, elem;\n const batchCmd = new BatchCommand('Clone Elements');\n // find all the elements selected (stop at first null)\n const len = selectedElements.length;\n function sortfunction (a, b) {\n return ($(b).index() - $(a).index()); // causes an array to be sorted numerically and ascending\n }\n selectedElements.sort(sortfunction);\n for (i = 0; i < len; ++i) {\n elem = selectedElements[i];\n if (elem == null) { break; }\n }\n // use slice to quickly get the subset of elements we need\n const copiedElements = selectedElements.slice(0, i);\n this.clearSelection(true);\n // note that we loop in the reverse way because of the way elements are added\n // to the selectedElements array (top-first)\n const drawing = getCurrentDrawing();\n i = copiedElements.length;\n while (i--) {\n // clone each element and replace it within copiedElements\n elem = copiedElements[i] = drawing.copyElem(copiedElements[i]);\n (currentGroup || drawing.getCurrentLayer()).appendChild(elem);\n batchCmd.addSubCommand(new InsertElementCommand(elem));\n }\n\n if (!batchCmd.isEmpty()) {\n addToSelection(copiedElements.reverse()); // Need to reverse for correct selection-adding\n this.moveSelectedElements(x, y, false);\n addCommandToHistory(batchCmd);\n }\n};\n\n/**\n* Aligns selected elements\n* @param {String} type - String with single character indicating the alignment type\n* @param {\"selected\"|\"largest\"|\"smallest\"|\"page\"} relativeTo\n*/\nthis.alignSelectedElements = function (type, relativeTo) {\n const bboxes = []; // angles = [];\n const len = selectedElements.length;\n if (!len) { return; }\n let minx = Number.MAX_VALUE, maxx = Number.MIN_VALUE,\n miny = Number.MAX_VALUE, maxy = Number.MIN_VALUE;\n let curwidth = Number.MIN_VALUE, curheight = Number.MIN_VALUE;\n for (let i = 0; i < len; ++i) {\n if (selectedElements[i] == null) { break; }\n const elem = selectedElements[i];\n bboxes[i] = getStrokedBBoxDefaultVisible([elem]);\n\n // now bbox is axis-aligned and handles rotation\n switch (relativeTo) {\n case 'smallest':\n if (((type === 'l' || type === 'c' || type === 'r') &&\n (curwidth === Number.MIN_VALUE || curwidth > bboxes[i].width)) ||\n ((type === 't' || type === 'm' || type === 'b') &&\n (curheight === Number.MIN_VALUE || curheight > bboxes[i].height))\n ) {\n minx = bboxes[i].x;\n miny = bboxes[i].y;\n maxx = bboxes[i].x + bboxes[i].width;\n maxy = bboxes[i].y + bboxes[i].height;\n curwidth = bboxes[i].width;\n curheight = bboxes[i].height;\n }\n break;\n case 'largest':\n if (((type === 'l' || type === 'c' || type === 'r') &&\n (curwidth === Number.MIN_VALUE || curwidth < bboxes[i].width)) ||\n ((type === 't' || type === 'm' || type === 'b') &&\n (curheight === Number.MIN_VALUE || curheight < bboxes[i].height))\n ) {\n minx = bboxes[i].x;\n miny = bboxes[i].y;\n maxx = bboxes[i].x + bboxes[i].width;\n maxy = bboxes[i].y + bboxes[i].height;\n curwidth = bboxes[i].width;\n curheight = bboxes[i].height;\n }\n break;\n default: // 'selected'\n if (bboxes[i].x < minx) { minx = bboxes[i].x; }\n if (bboxes[i].y < miny) { miny = bboxes[i].y; }\n if (bboxes[i].x + bboxes[i].width > maxx) { maxx = bboxes[i].x + bboxes[i].width; }\n if (bboxes[i].y + bboxes[i].height > maxy) { maxy = bboxes[i].y + bboxes[i].height; }\n break;\n }\n } // loop for each element to find the bbox and adjust min/max\n\n if (relativeTo === 'page') {\n minx = 0;\n miny = 0;\n maxx = canvas.contentW;\n maxy = canvas.contentH;\n }\n\n const dx = new Array(len);\n const dy = new Array(len);\n for (let i = 0; i < len; ++i) {\n if (selectedElements[i] == null) { break; }\n // const elem = selectedElements[i];\n const bbox = bboxes[i];\n dx[i] = 0;\n dy[i] = 0;\n switch (type) {\n case 'l': // left (horizontal)\n dx[i] = minx - bbox.x;\n break;\n case 'c': // center (horizontal)\n dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2);\n break;\n case 'r': // right (horizontal)\n dx[i] = maxx - (bbox.x + bbox.width);\n break;\n case 't': // top (vertical)\n dy[i] = miny - bbox.y;\n break;\n case 'm': // middle (vertical)\n dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2);\n break;\n case 'b': // bottom (vertical)\n dy[i] = maxy - (bbox.y + bbox.height);\n break;\n }\n }\n this.moveSelectedElements(dx, dy);\n};\n\n/**\n* Group: Additional editor tools\n*/\n\nthis.contentW = getResolution().w;\nthis.contentH = getResolution().h;\n\n/**\n* Updates the editor canvas width/height/position after a zoom has occurred\n* @param {Number} w - Float with the new width\n* @param {Number} h - Float with the new height\n* @returns Object with the following values:\n* - x - The canvas' new x coordinate\n* - y - The canvas' new y coordinate\n* - oldX - The canvas' old x coordinate\n* - oldY - The canvas' old y coordinate\n* - d_x - The x position difference\n* - d_y - The y position difference\n*/\nthis.updateCanvas = function (w, h) {\n svgroot.setAttribute('width', w);\n svgroot.setAttribute('height', h);\n const bg = $('#canvasBackground')[0];\n const oldX = svgcontent.getAttribute('x');\n const oldY = svgcontent.getAttribute('y');\n const x = (w / 2 - this.contentW * currentZoom / 2);\n const y = (h / 2 - this.contentH * currentZoom / 2);\n\n assignAttributes(svgcontent, {\n width: this.contentW * currentZoom,\n height: this.contentH * currentZoom,\n x,\n y,\n viewBox: '0 0 ' + this.contentW + ' ' + this.contentH\n });\n\n assignAttributes(bg, {\n width: svgcontent.getAttribute('width'),\n height: svgcontent.getAttribute('height'),\n x,\n y\n });\n\n const bgImg = getElem('background_image');\n if (bgImg) {\n assignAttributes(bgImg, {\n width: '100%',\n height: '100%'\n });\n }\n\n selectorManager.selectorParentGroup.setAttribute('transform', 'translate(' + x + ',' + y + ')');\n runExtensions('canvasUpdated', {new_x: x, new_y: y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY});\n return {x, y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY};\n};\n\n/**\n* Set the background of the editor (NOT the actual document)\n* @param {String} color - String with fill color to apply\n* @param url - URL or path to image to use\n*/\nthis.setBackground = function (color, url) {\n const bg = getElem('canvasBackground');\n const border = $(bg).find('rect')[0];\n let bgImg = getElem('background_image');\n border.setAttribute('fill', color);\n if (url) {\n if (!bgImg) {\n bgImg = svgdoc.createElementNS(NS.SVG, 'image');\n assignAttributes(bgImg, {\n id: 'background_image',\n width: '100%',\n height: '100%',\n preserveAspectRatio: 'xMinYMin',\n style: 'pointer-events:none'\n });\n }\n setHref(bgImg, url);\n bg.appendChild(bgImg);\n } else if (bgImg) {\n bgImg.parentNode.removeChild(bgImg);\n }\n};\n\n/**\n* Select the next/previous element within the current layer\n* @param {Boolean} next - true = next and false = previous element\n*/\nthis.cycleElement = function (next) {\n let num;\n const curElem = selectedElements[0];\n let elem = false;\n const allElems = getVisibleElements(currentGroup || getCurrentDrawing().getCurrentLayer());\n if (!allElems.length) { return; }\n if (curElem == null) {\n num = next ? allElems.length - 1 : 0;\n elem = allElems[num];\n } else {\n let i = allElems.length;\n while (i--) {\n if (allElems[i] === curElem) {\n num = next ? i - 1 : i + 1;\n if (num >= allElems.length) {\n num = 0;\n } else if (num < 0) {\n num = allElems.length - 1;\n }\n elem = allElems[num];\n break;\n }\n }\n }\n selectOnly([elem], true);\n call('selected', selectedElements);\n};\n\nthis.clear();\n\n/**\n* @deprecated getPrivateMethods\n* Since all methods are/should be public somehow, this function should be removed;\n* we might require `import` in place of this in the future once ES6 Modules\n* widespread\n\n* Being able to access private methods publicly seems wrong somehow,\n* but currently appears to be the best way to allow testing and provide\n* access to them to plugins.\n*/\nthis.getPrivateMethods = function () {\n const obj = {\n addCommandToHistory,\n setGradient,\n addSvgElementFromJson,\n assignAttributes,\n BatchCommand,\n buildCanvgCallback,\n call,\n canvg,\n ChangeElementCommand,\n copyElem (elem) { return getCurrentDrawing().copyElem(elem); },\n decode64,\n encode64,\n executeAfterLoads,\n ffClone,\n findDefs,\n findDuplicateGradient,\n getElem,\n getId,\n getIntersectionList,\n getMouseTarget,\n getNextId,\n getPathBBox,\n getTypeMap,\n getUrlFromAttr,\n hasMatrixTransform,\n identifyLayers: draw.identifyLayers,\n InsertElementCommand,\n isChrome,\n isIdentity,\n isIE,\n logMatrix,\n matrixMultiply,\n MoveElementCommand,\n NS,\n preventClickDefault,\n recalculateAllSelectedDimensions,\n recalculateDimensions,\n remapElement,\n RemoveElementCommand,\n removeUnusedDefElems,\n round,\n runExtensions,\n sanitizeSvg,\n SVGEditTransformList,\n text2xml,\n toString,\n transformBox,\n transformListToTransform,\n transformPoint,\n walkTree\n };\n return obj;\n};\n } // End constructor\n} // End class\n","/**\n * jGraduate 0.4\n *\n * jQuery Plugin for a gradient picker\n *\n * Copyright (c) 2010 Jeff Schiller\n * http://blog.codedread.com/\n * Copyright (c) 2010 Alexis Deveria\n * http://a.deveria.com/\n *\n * Apache 2 License\n\njGraduate(options, okCallback, cancelCallback)\n\nwhere options is an object literal:\n {\n window: { title: \"Pick the start color and opacity for the gradient\" },\n images: { clientPath: \"images/\" },\n paint: a Paint object,\n newstop: String of value \"same\", \"inverse\", \"black\" or \"white\"\n OR object with one or both values {color: #Hex color, opac: number 0-1}\n }\n\n- the Paint object is:\n Paint {\n type: String, // one of \"none\", \"solidColor\", \"linearGradient\", \"radialGradient\"\n alpha: Number representing opacity (0-100),\n solidColor: String representing #RRGGBB hex of color,\n linearGradient: object of interface SVGLinearGradientElement,\n radialGradient: object of interface SVGRadialGradientElement,\n }\n\n$.jGraduate.Paint() -> constructs a 'none' color\n$.jGraduate.Paint({copy: o}) -> creates a copy of the paint o\n$.jGraduate.Paint({hex: \"#rrggbb\"}) -> creates a solid color paint with hex = \"#rrggbb\"\n$.jGraduate.Paint({linearGradient: o, a: 50}) -> creates a linear gradient paint with opacity=0.5\n$.jGraduate.Paint({radialGradient: o, a: 7}) -> creates a radial gradient paint with opacity=0.07\n$.jGraduate.Paint({hex: \"#rrggbb\", linearGradient: o}) -> throws an exception?\n\n- picker accepts the following object as input:\n {\n okCallback: function to call when Ok is pressed\n cancelCallback: function to call when Cancel is pressed\n paint: object describing the paint to display initially, if not set, then default to opaque white\n }\n\n- okCallback receives a Paint object\n\n *\n*/\nconst ns = {\n svg: 'http://www.w3.org/2000/svg',\n xlink: 'http://www.w3.org/1999/xlink'\n};\n\nif (!window.console) {\n window.console = {\n log (str) {},\n dir (str) {}\n };\n}\n\nexport default function ($) {\n if (!$.loadingStylesheets) {\n $.loadingStylesheets = [];\n }\n const stylesheet = 'jgraduate/css/jgraduate.css';\n if (!$.loadingStylesheets.includes(stylesheet)) {\n $.loadingStylesheets.push(stylesheet);\n }\n $.jGraduate = {\n Paint: function (opt) {\n const options = opt || {};\n this.alpha = isNaN(options.alpha) ? 100 : options.alpha;\n // copy paint object\n if (options.copy) {\n this.type = options.copy.type;\n this.alpha = options.copy.alpha;\n this.solidColor = null;\n this.linearGradient = null;\n this.radialGradient = null;\n\n switch (this.type) {\n case 'none':\n break;\n case 'solidColor':\n this.solidColor = options.copy.solidColor;\n break;\n case 'linearGradient':\n this.linearGradient = options.copy.linearGradient.cloneNode(true);\n break;\n case 'radialGradient':\n this.radialGradient = options.copy.radialGradient.cloneNode(true);\n break;\n }\n // create linear gradient paint\n } else if (options.linearGradient) {\n this.type = 'linearGradient';\n this.solidColor = null;\n this.radialGradient = null;\n this.linearGradient = options.linearGradient.cloneNode(true);\n // create linear gradient paint\n } else if (options.radialGradient) {\n this.type = 'radialGradient';\n this.solidColor = null;\n this.linearGradient = null;\n this.radialGradient = options.radialGradient.cloneNode(true);\n // create solid color paint\n } else if (options.solidColor) {\n this.type = 'solidColor';\n this.solidColor = options.solidColor;\n // create empty paint\n } else {\n this.type = 'none';\n this.solidColor = null;\n this.linearGradient = null;\n this.radialGradient = null;\n }\n }\n };\n\n $.fn.jGraduateDefaults = {\n paint: new $.jGraduate.Paint(),\n window: {\n pickerTitle: 'Drag markers to pick a paint'\n },\n images: {\n clientPath: 'images/'\n },\n newstop: 'inverse' // same, inverse, black, white\n };\n\n const isGecko = navigator.userAgent.includes('Gecko/');\n\n function setAttrs (elem, attrs) {\n if (isGecko) {\n for (const aname in attrs) elem.setAttribute(aname, attrs[aname]);\n } else {\n for (const aname in attrs) {\n const val = attrs[aname], prop = elem[aname];\n if (prop && prop.constructor === 'SVGLength') {\n prop.baseVal.value = val;\n } else {\n elem.setAttribute(aname, val);\n }\n }\n }\n }\n\n function mkElem (name, attrs, newparent) {\n const elem = document.createElementNS(ns.svg, name);\n setAttrs(elem, attrs);\n if (newparent) newparent.appendChild(elem);\n return elem;\n }\n\n $.fn.jGraduate = function (options) {\n const $arguments = arguments;\n return this.each(function () {\n const $this = $(this),\n $settings = $.extend(true, {}, $.fn.jGraduateDefaults, options),\n id = $this.attr('id'),\n idref = '#' + $this.attr('id') + ' ';\n\n if (!idref) {\n alert('Container element must have an id attribute to maintain unique id strings for sub-elements.');\n return;\n }\n\n const okClicked = function () {\n switch ($this.paint.type) {\n case 'radialGradient':\n $this.paint.linearGradient = null;\n break;\n case 'linearGradient':\n $this.paint.radialGradient = null;\n break;\n case 'solidColor':\n $this.paint.radialGradient = $this.paint.linearGradient = null;\n break;\n }\n typeof $this.okCallback === 'function' && $this.okCallback($this.paint);\n $this.hide();\n };\n const cancelClicked = function () {\n typeof $this.cancelCallback === 'function' && $this.cancelCallback();\n $this.hide();\n };\n\n $.extend(true, $this, { // public properties, methods, and callbacks\n // make a copy of the incoming paint\n paint: new $.jGraduate.Paint({copy: $settings.paint}),\n okCallback: (typeof $arguments[1] === 'function' && $arguments[1]) || null,\n cancelCallback: (typeof $arguments[2] === 'function' && $arguments[2]) || null\n });\n\n let // pos = $this.position(),\n color = null;\n const $win = $(window);\n\n if ($this.paint.type === 'none') {\n $this.paint = $.jGraduate.Paint({solidColor: 'ffffff'});\n }\n\n $this.addClass('jGraduate_Picker');\n $this.html(\n '
      ' +\n '
    • Solid Color
    • ' +\n '
    • Linear Gradient
    • ' +\n '
    • Radial Gradient
    • ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    '\n );\n const colPicker = $(idref + '> .jGraduate_colPick');\n const gradPicker = $(idref + '> .jGraduate_gradPick');\n\n gradPicker.html(\n '
    ' +\n '

    ' + $settings.window.pickerTitle + '

    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '
    ');\n\n // --------------\n // Set up all the SVG elements (the gradient, stops and rectangle)\n const MAX = 256,\n MARGINX = 0,\n MARGINY = 0,\n // STOP_RADIUS = 15 / 2,\n SIZEX = MAX - 2 * MARGINX,\n SIZEY = MAX - 2 * MARGINY;\n\n const attrInput = {};\n\n const SLIDERW = 145;\n $('.jGraduate_SliderBar').width(SLIDERW);\n\n const container = $('#' + id + '_jGraduate_GradContainer')[0];\n\n const svg = mkElem('svg', {\n id: id + '_jgraduate_svg',\n width: MAX,\n height: MAX,\n xmlns: ns.svg\n }, container);\n\n // This wasn't working as designed\n // let curType;\n // curType = curType || $this.paint.type;\n\n // if we are sent a gradient, import it\n let curType = $this.paint.type;\n\n let grad = $this.paint[curType];\n let curGradient = grad;\n\n const gradalpha = $this.paint.alpha;\n\n const isSolid = curType === 'solidColor';\n\n // Make any missing gradients\n switch (curType) {\n case 'solidColor':\n // fall through\n case 'linearGradient':\n if (!isSolid) {\n curGradient.id = id + '_lg_jgraduate_grad';\n grad = curGradient = svg.appendChild(curGradient); // .cloneNode(true));\n }\n mkElem('radialGradient', {\n id: id + '_rg_jgraduate_grad'\n }, svg);\n if (curType === 'linearGradient') { break; }\n // fall through\n case 'radialGradient':\n if (!isSolid) {\n curGradient.id = id + '_rg_jgraduate_grad';\n grad = curGradient = svg.appendChild(curGradient); // .cloneNode(true));\n }\n mkElem('linearGradient', {\n id: id + '_lg_jgraduate_grad'\n }, svg);\n }\n\n let stopGroup; // eslint-disable-line prefer-const\n if (isSolid) {\n grad = curGradient = $('#' + id + '_lg_jgraduate_grad')[0];\n color = $this.paint[curType];\n mkStop(0, '#' + color, 1);\n\n const type = typeof $settings.newstop;\n\n if (type === 'string') {\n switch ($settings.newstop) {\n case 'same':\n mkStop(1, '#' + color, 1);\n break;\n\n case 'inverse':\n // Invert current color for second stop\n let inverted = '';\n for (let i = 0; i < 6; i += 2) {\n // const ch = color.substr(i, 2);\n let inv = (255 - parseInt(color.substr(i, 2), 16)).toString(16);\n if (inv.length < 2) inv = 0 + inv;\n inverted += inv;\n }\n mkStop(1, '#' + inverted, 1);\n break;\n\n case 'white':\n mkStop(1, '#ffffff', 1);\n break;\n\n case 'black':\n mkStop(1, '#000000', 1);\n break;\n }\n } else if (type === 'object') {\n const opac = ('opac' in $settings.newstop) ? $settings.newstop.opac : 1;\n mkStop(1, ($settings.newstop.color || '#' + color), opac);\n }\n }\n\n const x1 = parseFloat(grad.getAttribute('x1') || 0.0),\n y1 = parseFloat(grad.getAttribute('y1') || 0.0),\n x2 = parseFloat(grad.getAttribute('x2') || 1.0),\n y2 = parseFloat(grad.getAttribute('y2') || 0.0);\n\n const cx = parseFloat(grad.getAttribute('cx') || 0.5),\n cy = parseFloat(grad.getAttribute('cy') || 0.5),\n fx = parseFloat(grad.getAttribute('fx') || cx),\n fy = parseFloat(grad.getAttribute('fy') || cy);\n\n const previewRect = mkElem('rect', {\n id: id + '_jgraduate_rect',\n x: MARGINX,\n y: MARGINY,\n width: SIZEX,\n height: SIZEY,\n fill: 'url(#' + id + '_jgraduate_grad)',\n 'fill-opacity': gradalpha / 100\n }, svg);\n\n // stop visuals created here\n const beginCoord = $('
    ').attr({\n class: 'grad_coord jGraduate_lg_field',\n title: 'Begin Stop'\n }).text(1).css({\n top: y1 * MAX,\n left: x1 * MAX\n }).data('coord', 'start').appendTo(container);\n\n const endCoord = beginCoord.clone().text(2).css({\n top: y2 * MAX,\n left: x2 * MAX\n }).attr('title', 'End stop').data('coord', 'end').appendTo(container);\n\n const centerCoord = $('
    ').attr({\n class: 'grad_coord jGraduate_rg_field',\n title: 'Center stop'\n }).text('C').css({\n top: cy * MAX,\n left: cx * MAX\n }).data('coord', 'center').appendTo(container);\n\n const focusCoord = centerCoord.clone().text('F').css({\n top: fy * MAX,\n left: fx * MAX,\n display: 'none'\n }).attr('title', 'Focus point').data('coord', 'focus').appendTo(container);\n\n focusCoord[0].id = id + '_jGraduate_focusCoord';\n\n // const coords = $(idref + ' .grad_coord');\n\n // $(container).hover(function () {\n // coords.animate({\n // opacity: 1\n // }, 500);\n // }, function () {\n // coords.animate({\n // opacity: .2\n // }, 500);\n // });\n\n let showFocus;\n $.each(['x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy'], function (i, attr) {\n const isRadial = isNaN(attr[1]);\n\n let attrval = curGradient.getAttribute(attr);\n if (!attrval) {\n // Set defaults\n if (isRadial) {\n // For radial points\n attrval = '0.5';\n } else {\n // Only x2 is 1\n attrval = attr === 'x2' ? '1.0' : '0.0';\n }\n }\n\n attrInput[attr] = $('#' + id + '_jGraduate_' + attr)\n .val(attrval)\n .change(function () {\n // TODO: Support values < 0 and > 1 (zoomable preview?)\n if (isNaN(parseFloat(this.value)) || this.value < 0) {\n this.value = 0.0;\n } else if (this.value > 1) {\n this.value = 1.0;\n }\n\n if (!(attr[0] === 'f' && !showFocus)) {\n if ((isRadial && curType === 'radialGradient') || (!isRadial && curType === 'linearGradient')) {\n curGradient.setAttribute(attr, this.value);\n }\n }\n\n const $elem = isRadial\n ? attr[0] === 'c' ? centerCoord : focusCoord\n : attr[1] === '1' ? beginCoord : endCoord;\n\n const cssName = attr.includes('x') ? 'left' : 'top';\n\n $elem.css(cssName, this.value * MAX);\n }).change();\n });\n\n function mkStop (n, color, opac, sel, stopElem) {\n const stop = stopElem || mkElem('stop', {'stop-color': color, 'stop-opacity': opac, offset: n}, curGradient);\n if (stopElem) {\n color = stopElem.getAttribute('stop-color');\n opac = stopElem.getAttribute('stop-opacity');\n n = stopElem.getAttribute('offset');\n } else {\n curGradient.appendChild(stop);\n }\n if (opac === null) opac = 1;\n\n const pickerD = 'M-6.2,0.9c3.6-4,6.7-4.3,6.7-12.4c-0.2,7.9,3.1,8.8,6.5,12.4c3.5,3.8,2.9,9.6,0,12.3c-3.1,2.8-10.4,2.7-13.2,0C-9.6,9.9-9.4,4.4-6.2,0.9z';\n\n const pathbg = mkElem('path', {\n d: pickerD,\n fill: 'url(#jGraduate_trans)',\n transform: 'translate(' + (10 + n * MAX) + ', 26)'\n }, stopGroup);\n\n const path = mkElem('path', {\n d: pickerD,\n fill: color,\n 'fill-opacity': opac,\n transform: 'translate(' + (10 + n * MAX) + ', 26)',\n stroke: '#000',\n 'stroke-width': 1.5\n }, stopGroup);\n\n $(path).mousedown(function (e) {\n selectStop(this);\n drag = curStop;\n $win.mousemove(dragColor).mouseup(remDrags);\n stopOffset = stopMakerDiv.offset();\n e.preventDefault();\n return false;\n }).data('stop', stop).data('bg', pathbg).dblclick(function () {\n $('div.jGraduate_LightBox').show();\n const colorhandle = this;\n let stopOpacity = +stop.getAttribute('stop-opacity') || 1;\n let stopColor = stop.getAttribute('stop-color') || 1;\n let thisAlpha = (parseFloat(stopOpacity) * 255).toString(16);\n while (thisAlpha.length < 2) { thisAlpha = '0' + thisAlpha; }\n color = stopColor.substr(1) + thisAlpha;\n $('#' + id + '_jGraduate_stopPicker').css({left: 100, bottom: 15}).jPicker({\n window: { title: 'Pick the start color and opacity for the gradient' },\n images: { clientPath: $settings.images.clientPath },\n color: { active: color, alphaSupport: true }\n }, function (color, arg2) {\n stopColor = color.val('hex') ? ('#' + color.val('hex')) : 'none';\n stopOpacity = color.val('a') !== null ? color.val('a') / 256 : 1;\n colorhandle.setAttribute('fill', stopColor);\n colorhandle.setAttribute('fill-opacity', stopOpacity);\n stop.setAttribute('stop-color', stopColor);\n stop.setAttribute('stop-opacity', stopOpacity);\n $('div.jGraduate_LightBox').hide();\n $('#' + id + '_jGraduate_stopPicker').hide();\n }, null, function () {\n $('div.jGraduate_LightBox').hide();\n $('#' + id + '_jGraduate_stopPicker').hide();\n });\n });\n\n $(curGradient).find('stop').each(function () {\n const curS = $(this);\n if (+this.getAttribute('offset') > n) {\n if (!color) {\n const newcolor = this.getAttribute('stop-color');\n const newopac = this.getAttribute('stop-opacity');\n stop.setAttribute('stop-color', newcolor);\n path.setAttribute('fill', newcolor);\n stop.setAttribute('stop-opacity', newopac === null ? 1 : newopac);\n path.setAttribute('fill-opacity', newopac === null ? 1 : newopac);\n }\n curS.before(stop);\n return false;\n }\n });\n if (sel) selectStop(path);\n return stop;\n }\n\n function remStop () {\n delStop.setAttribute('display', 'none');\n const path = $(curStop);\n const stop = path.data('stop');\n const bg = path.data('bg');\n $([curStop, stop, bg]).remove();\n }\n\n const stopMakerDiv = $('#' + id + '_jGraduate_StopSlider');\n\n let stops, curStop, drag;\n\n const delStop = mkElem('path', {\n d: 'm9.75,-6l-19.5,19.5m0,-19.5l19.5,19.5',\n fill: 'none',\n stroke: '#D00',\n 'stroke-width': 5,\n display: 'none'\n }, undefined); // stopMakerSVG);\n\n function selectStop (item) {\n if (curStop) curStop.setAttribute('stroke', '#000');\n item.setAttribute('stroke', 'blue');\n curStop = item;\n curStop.parentNode.appendChild(curStop);\n // stops = $('stop');\n // opac_select.val(curStop.attr('fill-opacity') || 1);\n // root.append(delStop);\n }\n\n let stopOffset;\n\n function remDrags () {\n $win.unbind('mousemove', dragColor);\n if (delStop.getAttribute('display') !== 'none') {\n remStop();\n }\n drag = null;\n }\n\n let scaleX = 1, scaleY = 1, angle = 0;\n\n let cX = cx;\n let cY = cy;\n function xform () {\n const rot = angle ? 'rotate(' + angle + ',' + cX + ',' + cY + ') ' : '';\n if (scaleX === 1 && scaleY === 1) {\n curGradient.removeAttribute('gradientTransform');\n // $('#ang').addClass('dis');\n } else {\n const x = -cX * (scaleX - 1);\n const y = -cY * (scaleY - 1);\n curGradient.setAttribute('gradientTransform', rot + 'translate(' + x + ',' + y + ') scale(' + scaleX + ',' + scaleY + ')');\n // $('#ang').removeClass('dis');\n }\n }\n\n function dragColor (evt) {\n let x = evt.pageX - stopOffset.left;\n const y = evt.pageY - stopOffset.top;\n x = x < 10\n ? 10\n : x > MAX + 10\n ? MAX + 10\n : x;\n\n const xfStr = 'translate(' + x + ', 26)';\n if (y < -60 || y > 130) {\n delStop.setAttribute('display', 'block');\n delStop.setAttribute('transform', xfStr);\n } else {\n delStop.setAttribute('display', 'none');\n }\n\n drag.setAttribute('transform', xfStr);\n $.data(drag, 'bg').setAttribute('transform', xfStr);\n const stop = $.data(drag, 'stop');\n const sX = (x - 10) / MAX;\n\n stop.setAttribute('offset', sX);\n\n let last = 0;\n $(curGradient).find('stop').each(function (i) {\n const cur = this.getAttribute('offset');\n const t = $(this);\n if (cur < last) {\n t.prev().before(t);\n stops = $(curGradient).find('stop');\n }\n last = cur;\n });\n }\n\n const stopMakerSVG = mkElem('svg', {\n width: '100%',\n height: 45\n }, stopMakerDiv[0]);\n\n const transPattern = mkElem('pattern', {\n width: 16,\n height: 16,\n patternUnits: 'userSpaceOnUse',\n id: 'jGraduate_trans'\n }, stopMakerSVG);\n\n const transImg = mkElem('image', {\n width: 16,\n height: 16\n }, transPattern);\n\n const bgImage = $settings.images.clientPath + 'map-opacity.png';\n\n transImg.setAttributeNS(ns.xlink, 'xlink:href', bgImage);\n\n $(stopMakerSVG).click(function (evt) {\n stopOffset = stopMakerDiv.offset();\n const {target} = evt;\n if (target.tagName === 'path') return;\n let x = evt.pageX - stopOffset.left - 8;\n x = x < 10 ? 10 : x > MAX + 10 ? MAX + 10 : x;\n mkStop(x / MAX, 0, 0, true);\n evt.stopPropagation();\n });\n\n $(stopMakerSVG).mouseover(function () {\n stopMakerSVG.appendChild(delStop);\n });\n\n stopGroup = mkElem('g', {}, stopMakerSVG);\n\n mkElem('line', {\n x1: 10,\n y1: 15,\n x2: MAX + 10,\n y2: 15,\n 'stroke-width': 2,\n stroke: '#000'\n }, stopMakerSVG);\n\n const spreadMethodOpt = gradPicker.find('.jGraduate_spreadMethod').change(function () {\n curGradient.setAttribute('spreadMethod', $(this).val());\n });\n\n // handle dragging the stop around the swatch\n let draggingCoord = null;\n\n const onCoordDrag = function (evt) {\n let x = evt.pageX - offset.left;\n let y = evt.pageY - offset.top;\n\n // clamp stop to the swatch\n x = x < 0 ? 0 : x > MAX ? MAX : x;\n y = y < 0 ? 0 : y > MAX ? MAX : y;\n\n draggingCoord.css('left', x).css('top', y);\n\n // calculate stop offset\n const fracx = x / SIZEX;\n const fracy = y / SIZEY;\n\n const type = draggingCoord.data('coord');\n const grad = curGradient;\n\n switch (type) {\n case 'start':\n attrInput.x1.val(fracx);\n attrInput.y1.val(fracy);\n grad.setAttribute('x1', fracx);\n grad.setAttribute('y1', fracy);\n break;\n case 'end':\n attrInput.x2.val(fracx);\n attrInput.y2.val(fracy);\n grad.setAttribute('x2', fracx);\n grad.setAttribute('y2', fracy);\n break;\n case 'center':\n attrInput.cx.val(fracx);\n attrInput.cy.val(fracy);\n grad.setAttribute('cx', fracx);\n grad.setAttribute('cy', fracy);\n cX = fracx;\n cY = fracy;\n xform();\n break;\n case 'focus':\n attrInput.fx.val(fracx);\n attrInput.fy.val(fracy);\n grad.setAttribute('fx', fracx);\n grad.setAttribute('fy', fracy);\n xform();\n }\n\n evt.preventDefault();\n };\n\n const onCoordUp = function () {\n draggingCoord = null;\n $win.unbind('mousemove', onCoordDrag).unbind('mouseup', onCoordUp);\n };\n\n // Linear gradient\n // (function () {\n\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n\n let numstops = stops.length;\n // if there are not at least two stops, then\n if (numstops < 2) {\n while (numstops < 2) {\n curGradient.appendChild(document.createElementNS(ns.svg, 'stop'));\n ++numstops;\n }\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n }\n\n for (let i = 0; i < numstops; i++) {\n mkStop(0, 0, 0, 0, stops[i]);\n }\n\n spreadMethodOpt.val(curGradient.getAttribute('spreadMethod') || 'pad');\n\n let offset;\n\n // No match, so show focus point\n showFocus = false;\n\n previewRect.setAttribute('fill-opacity', gradalpha / 100);\n\n $('#' + id + ' div.grad_coord').mousedown(function (evt) {\n evt.preventDefault();\n draggingCoord = $(this);\n // const sPos = draggingCoord.offset();\n offset = draggingCoord.parent().offset();\n $win.mousemove(onCoordDrag).mouseup(onCoordUp);\n });\n\n // bind GUI elements\n $('#' + id + '_jGraduate_Ok').bind('click', function () {\n $this.paint.type = curType;\n $this.paint[curType] = curGradient.cloneNode(true);\n $this.paint.solidColor = null;\n okClicked();\n });\n $('#' + id + '_jGraduate_Cancel').bind('click', function (paint) {\n cancelClicked();\n });\n\n if (curType === 'radialGradient') {\n if (showFocus) {\n focusCoord.show();\n } else {\n focusCoord.hide();\n attrInput.fx.val('');\n attrInput.fy.val('');\n }\n }\n\n $('#' + id + '_jGraduate_match_ctr')[0].checked = !showFocus;\n\n let lastfx, lastfy;\n\n $('#' + id + '_jGraduate_match_ctr').change(function () {\n showFocus = !this.checked;\n focusCoord.toggle(showFocus);\n attrInput.fx.val('');\n attrInput.fy.val('');\n const grad = curGradient;\n if (!showFocus) {\n lastfx = grad.getAttribute('fx');\n lastfy = grad.getAttribute('fy');\n grad.removeAttribute('fx');\n grad.removeAttribute('fy');\n } else {\n const fx = lastfx || 0.5;\n const fy = lastfy || 0.5;\n grad.setAttribute('fx', fx);\n grad.setAttribute('fy', fy);\n attrInput.fx.val(fx);\n attrInput.fy.val(fy);\n }\n });\n\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n numstops = stops.length;\n // if there are not at least two stops, then\n if (numstops < 2) {\n while (numstops < 2) {\n curGradient.appendChild(document.createElementNS(ns.svg, 'stop'));\n ++numstops;\n }\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n }\n\n let slider;\n\n const setSlider = function (e) {\n const {offset} = slider;\n const div = slider.parent;\n let x = (e.pageX - offset.left - parseInt(div.css('border-left-width')));\n if (x > SLIDERW) x = SLIDERW;\n if (x <= 0) x = 0;\n const posx = x - 5;\n x /= SLIDERW;\n\n switch (slider.type) {\n case 'radius':\n x = Math.pow(x * 2, 2.5);\n if (x > 0.98 && x < 1.02) x = 1;\n if (x <= 0.01) x = 0.01;\n curGradient.setAttribute('r', x);\n break;\n case 'opacity':\n $this.paint.alpha = parseInt(x * 100);\n previewRect.setAttribute('fill-opacity', x);\n break;\n case 'ellip':\n scaleX = 1;\n scaleY = 1;\n if (x < 0.5) {\n x /= 0.5; // 0.001\n scaleX = x <= 0 ? 0.01 : x;\n } else if (x > 0.5) {\n x /= 0.5; // 2\n x = 2 - x;\n scaleY = x <= 0 ? 0.01 : x;\n }\n xform();\n x -= 1;\n if (scaleY === x + 1) {\n x = Math.abs(x);\n }\n break;\n case 'angle':\n x = x - 0.5;\n angle = x *= 180;\n xform();\n x /= 100;\n break;\n }\n slider.elem.css({'margin-left': posx});\n x = Math.round(x * 100);\n slider.input.val(x);\n };\n\n let ellipVal = 0, angleVal = 0;\n\n if (curType === 'radialGradient') {\n const tlist = curGradient.gradientTransform.baseVal;\n if (tlist.numberOfItems === 2) {\n const t = tlist.getItem(0);\n const s = tlist.getItem(1);\n if (t.type === 2 && s.type === 3) {\n const m = s.matrix;\n if (m.a !== 1) {\n ellipVal = Math.round(-(1 - m.a) * 100);\n } else if (m.d !== 1) {\n ellipVal = Math.round((1 - m.d) * 100);\n }\n }\n } else if (tlist.numberOfItems === 3) {\n // Assume [R][T][S]\n const r = tlist.getItem(0);\n const t = tlist.getItem(1);\n const s = tlist.getItem(2);\n\n if (r.type === 4 &&\n t.type === 2 &&\n s.type === 3\n ) {\n angleVal = Math.round(r.angle);\n const m = s.matrix;\n if (m.a !== 1) {\n ellipVal = Math.round(-(1 - m.a) * 100);\n } else if (m.d !== 1) {\n ellipVal = Math.round((1 - m.d) * 100);\n }\n }\n }\n }\n\n const sliders = {\n radius: {\n handle: '#' + id + '_jGraduate_RadiusArrows',\n input: '#' + id + '_jGraduate_RadiusInput',\n val: (curGradient.getAttribute('r') || 0.5) * 100\n },\n opacity: {\n handle: '#' + id + '_jGraduate_OpacArrows',\n input: '#' + id + '_jGraduate_OpacInput',\n val: $this.paint.alpha || 100\n },\n ellip: {\n handle: '#' + id + '_jGraduate_EllipArrows',\n input: '#' + id + '_jGraduate_EllipInput',\n val: ellipVal\n },\n angle: {\n handle: '#' + id + '_jGraduate_AngleArrows',\n input: '#' + id + '_jGraduate_AngleInput',\n val: angleVal\n }\n };\n\n $.each(sliders, function (type, data) {\n const handle = $(data.handle);\n handle.mousedown(function (evt) {\n const parent = handle.parent();\n slider = {\n type,\n elem: handle,\n input: $(data.input),\n parent,\n offset: parent.offset()\n };\n $win.mousemove(dragSlider).mouseup(stopSlider);\n evt.preventDefault();\n });\n\n $(data.input).val(data.val).change(function () {\n const isRad = curType === 'radialGradient';\n let val = +this.value;\n let xpos = 0;\n switch (type) {\n case 'radius':\n if (isRad) curGradient.setAttribute('r', val / 100);\n xpos = (Math.pow(val / 100, 1 / 2.5) / 2) * SLIDERW;\n break;\n\n case 'opacity':\n $this.paint.alpha = val;\n previewRect.setAttribute('fill-opacity', val / 100);\n xpos = val * (SLIDERW / 100);\n break;\n\n case 'ellip':\n scaleX = scaleY = 1;\n if (val === 0) {\n xpos = SLIDERW * 0.5;\n break;\n }\n if (val > 99.5) val = 99.5;\n if (val > 0) {\n scaleY = 1 - (val / 100);\n } else {\n scaleX = -(val / 100) - 1;\n }\n\n xpos = SLIDERW * ((val + 100) / 2) / 100;\n if (isRad) xform();\n break;\n\n case 'angle':\n angle = val;\n xpos = angle / 180;\n xpos += 0.5;\n xpos *= SLIDERW;\n if (isRad) xform();\n }\n if (xpos > SLIDERW) {\n xpos = SLIDERW;\n } else if (xpos < 0) {\n xpos = 0;\n }\n handle.css({'margin-left': xpos - 5});\n }).change();\n });\n\n const dragSlider = function (evt) {\n setSlider(evt);\n evt.preventDefault();\n };\n\n const stopSlider = function (evt) {\n $win.unbind('mousemove', dragSlider).unbind('mouseup', stopSlider);\n slider = null;\n };\n\n // --------------\n let thisAlpha = ($this.paint.alpha * 255 / 100).toString(16);\n while (thisAlpha.length < 2) { thisAlpha = '0' + thisAlpha; }\n thisAlpha = thisAlpha.split('.')[0];\n color = $this.paint.solidColor === 'none' ? '' : $this.paint.solidColor + thisAlpha;\n\n if (!isSolid) {\n color = stops[0].getAttribute('stop-color');\n }\n\n // This should be done somewhere else, probably\n $.extend($.fn.jPicker.defaults.window, {\n alphaSupport: true, effects: {type: 'show', speed: 0}\n });\n\n colPicker.jPicker(\n {\n window: { title: $settings.window.pickerTitle },\n images: { clientPath: $settings.images.clientPath },\n color: { active: color, alphaSupport: true }\n },\n function (color) {\n $this.paint.type = 'solidColor';\n $this.paint.alpha = color.val('ahex') ? Math.round((color.val('a') / 255) * 100) : 100;\n $this.paint.solidColor = color.val('hex') ? color.val('hex') : 'none';\n $this.paint.radialGradient = null;\n okClicked();\n },\n null,\n function () { cancelClicked(); }\n );\n\n const tabs = $(idref + ' .jGraduate_tabs li');\n tabs.click(function () {\n tabs.removeClass('jGraduate_tab_current');\n $(this).addClass('jGraduate_tab_current');\n $(idref + ' > div').hide();\n const type = $(this).attr('data-type');\n /* const container = */ $(idref + ' .jGraduate_gradPick').show();\n if (type === 'rg' || type === 'lg') {\n // Show/hide appropriate fields\n $('.jGraduate_' + type + '_field').show();\n $('.jGraduate_' + (type === 'lg' ? 'rg' : 'lg') + '_field').hide();\n\n $('#' + id + '_jgraduate_rect')[0].setAttribute('fill', 'url(#' + id + '_' + type + '_jgraduate_grad)');\n\n // Copy stops\n\n curType = type === 'lg' ? 'linearGradient' : 'radialGradient';\n\n $('#' + id + '_jGraduate_OpacInput').val($this.paint.alpha).change();\n\n const newGrad = $('#' + id + '_' + type + '_jgraduate_grad')[0];\n\n if (curGradient !== newGrad) {\n const curStops = $(curGradient).find('stop');\n $(newGrad).empty().append(curStops);\n curGradient = newGrad;\n const sm = spreadMethodOpt.val();\n curGradient.setAttribute('spreadMethod', sm);\n }\n showFocus = type === 'rg' && curGradient.getAttribute('fx') != null && !(cx === fx && cy === fy);\n $('#' + id + '_jGraduate_focusCoord').toggle(showFocus);\n if (showFocus) {\n $('#' + id + '_jGraduate_match_ctr')[0].checked = false;\n }\n } else {\n $(idref + ' .jGraduate_gradPick').hide();\n $(idref + ' .jGraduate_colPick').show();\n }\n });\n $(idref + ' > div').hide();\n tabs.removeClass('jGraduate_tab_current');\n let tab;\n switch ($this.paint.type) {\n case 'linearGradient':\n tab = $(idref + ' .jGraduate_tab_lingrad');\n break;\n case 'radialGradient':\n tab = $(idref + ' .jGraduate_tab_radgrad');\n break;\n default:\n tab = $(idref + ' .jGraduate_tab_color');\n break;\n }\n $this.show();\n\n // jPicker will try to show after a 0ms timeout, so need to fire this after that\n setTimeout(function () {\n tab.addClass('jGraduate_tab_current').click();\n }, 10);\n });\n };\n return $;\n}\n","/*\n * jPicker (Adapted from version 1.1.6)\n *\n * jQuery Plugin for Photoshop style color picker\n *\n * Copyright (c) 2010 Christopher T. Tillman\n * Digital Magic Productions, Inc. (http://www.digitalmagicpro.com/)\n * MIT style license, FREE to use, alter, copy, sell, and especially ENHANCE\n *\n * Painstakingly ported from John Dyers' excellent work on his own color picker based on the Prototype framework.\n *\n * John Dyers' website: (http://johndyer.name)\n * Color Picker page: (http://johndyer.name/post/2007/09/PhotoShop-like-JavaScript-Color-Picker.aspx)\n *\n */\n\nMath.precision = function (value, precision) {\n if (precision === undefined) precision = 0;\n return Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision);\n};\n\nexport default function ($) {\n if (!$.loadingStylesheets) {\n $.loadingStylesheets = [];\n }\n const stylesheet = 'jgraduate/css/jPicker.css';\n if (!$.loadingStylesheets.includes(stylesheet)) {\n $.loadingStylesheets.push(stylesheet);\n }\n /**\n * Encapsulate slider functionality for the ColorMap and ColorBar -\n * could be useful to use a jQuery UI draggable for this with certain extensions\n */\n function Slider (bar, options) {\n const $this = this;\n function fireChangeEvents (context) {\n for (let i = 0; i < changeEvents.length; i++) {\n changeEvents[i].call($this, $this, context);\n }\n }\n // bind the mousedown to the bar not the arrow for quick snapping to the clicked location\n function mouseDown (e) {\n const off = bar.offset();\n offset = {l: off.left | 0, t: off.top | 0};\n clearTimeout(timeout);\n // using setTimeout for visual updates - once the style is updated the browser will re-render internally allowing the next Javascript to run\n timeout = setTimeout(function () {\n setValuesFromMousePosition.call($this, e);\n }, 0);\n // Bind mousemove and mouseup event to the document so it responds when dragged of of the bar - we will unbind these when on mouseup to save processing\n $(document).bind('mousemove', mouseMove).bind('mouseup', mouseUp);\n e.preventDefault(); // don't try to select anything or drag the image to the desktop\n }\n // set the values as the mouse moves\n function mouseMove (e) {\n clearTimeout(timeout);\n timeout = setTimeout(function () {\n setValuesFromMousePosition.call($this, e);\n }, 0);\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n // unbind the document events - they aren't needed when not dragging\n function mouseUp (e) {\n $(document).unbind('mouseup', mouseUp).unbind('mousemove', mouseMove);\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n // calculate mouse position and set value within the current range\n function setValuesFromMousePosition (e) {\n const barW = bar.w, // local copies for YUI compressor\n barH = bar.h;\n let locX = e.pageX - offset.l,\n locY = e.pageY - offset.t;\n // keep the arrow within the bounds of the bar\n if (locX < 0) locX = 0;\n else if (locX > barW) locX = barW;\n if (locY < 0) locY = 0;\n else if (locY > barH) locY = barH;\n val.call($this, 'xy', { x: ((locX / barW) * rangeX) + minX, y: ((locY / barH) * rangeY) + minY });\n }\n function draw () {\n const\n barW = bar.w,\n barH = bar.h,\n arrowW = arrow.w,\n arrowH = arrow.h;\n let arrowOffsetX = 0,\n arrowOffsetY = 0;\n setTimeout(function () {\n if (rangeX > 0) { // range is greater than zero\n // constrain to bounds\n if (x === maxX) arrowOffsetX = barW;\n else arrowOffsetX = ((x / rangeX) * barW) | 0;\n }\n if (rangeY > 0) { // range is greater than zero\n // constrain to bounds\n if (y === maxY) arrowOffsetY = barH;\n else arrowOffsetY = ((y / rangeY) * barH) | 0;\n }\n // if arrow width is greater than bar width, center arrow and prevent horizontal dragging\n if (arrowW >= barW) arrowOffsetX = (barW >> 1) - (arrowW >> 1); // number >> 1 - superfast bitwise divide by two and truncate (move bits over one bit discarding lowest)\n else arrowOffsetX -= arrowW >> 1;\n // if arrow height is greater than bar height, center arrow and prevent vertical dragging\n if (arrowH >= barH) arrowOffsetY = (barH >> 1) - (arrowH >> 1);\n else arrowOffsetY -= arrowH >> 1;\n // set the arrow position based on these offsets\n arrow.css({ left: arrowOffsetX + 'px', top: arrowOffsetY + 'px' });\n }, 0);\n }\n function val (name, value, context) {\n const set = value !== undefined;\n if (!set) {\n if (name === undefined || name == null) name = 'xy';\n switch (name.toLowerCase()) {\n case 'x': return x;\n case 'y': return y;\n case 'xy':\n default: return { x, y };\n }\n }\n if (context != null && context === $this) return;\n let changed = false;\n\n let newX, newY;\n if (name == null) name = 'xy';\n switch (name.toLowerCase()) {\n case 'x':\n newX = (value && ((value.x && value.x | 0) || value | 0)) || 0;\n break;\n case 'y':\n newY = (value && ((value.y && value.y | 0) || value | 0)) || 0;\n break;\n case 'xy':\n default:\n newX = (value && value.x && value.x | 0) || 0;\n newY = (value && value.y && value.y | 0) || 0;\n break;\n }\n if (newX != null) {\n if (newX < minX) newX = minX;\n else if (newX > maxX) newX = maxX;\n if (x !== newX) {\n x = newX;\n changed = true;\n }\n }\n if (newY != null) {\n if (newY < minY) newY = minY;\n else if (newY > maxY) newY = maxY;\n if (y !== newY) {\n y = newY;\n changed = true;\n }\n }\n changed && fireChangeEvents.call($this, context || $this);\n }\n function range (name, value) {\n const set = value !== undefined;\n if (!set) {\n if (name === undefined || name == null) name = 'all';\n switch (name.toLowerCase()) {\n case 'minx': return minX;\n case 'maxx': return maxX;\n case 'rangex': return { minX, maxX, rangeX };\n case 'miny': return minY;\n case 'maxy': return maxY;\n case 'rangey': return { minY, maxY, rangeY };\n case 'all':\n default: return { minX, maxX, rangeX, minY, maxY, rangeY };\n }\n }\n let // changed = false,\n newMinX,\n newMaxX,\n newMinY,\n newMaxY;\n if (name == null) name = 'all';\n switch (name.toLowerCase()) {\n case 'minx':\n newMinX = (value && ((value.minX && value.minX | 0) || value | 0)) || 0;\n break;\n case 'maxx':\n newMaxX = (value && ((value.maxX && value.maxX | 0) || value | 0)) || 0;\n break;\n case 'rangex':\n newMinX = (value && value.minX && value.minX | 0) || 0;\n newMaxX = (value && value.maxX && value.maxX | 0) || 0;\n break;\n case 'miny':\n newMinY = (value && ((value.minY && value.minY | 0) || value | 0)) || 0;\n break;\n case 'maxy':\n newMaxY = (value && ((value.maxY && value.maxY | 0) || value | 0)) || 0;\n break;\n case 'rangey':\n newMinY = (value && value.minY && value.minY | 0) || 0;\n newMaxY = (value && value.maxY && value.maxY | 0) || 0;\n break;\n case 'all':\n default:\n newMinX = (value && value.minX && value.minX | 0) || 0;\n newMaxX = (value && value.maxX && value.maxX | 0) || 0;\n newMinY = (value && value.minY && value.minY | 0) || 0;\n newMaxY = (value && value.maxY && value.maxY | 0) || 0;\n break;\n }\n if (newMinX != null && minX !== newMinX) {\n minX = newMinX;\n rangeX = maxX - minX;\n }\n if (newMaxX != null && maxX !== newMaxX) {\n maxX = newMaxX;\n rangeX = maxX - minX;\n }\n if (newMinY != null && minY !== newMinY) {\n minY = newMinY;\n rangeY = maxY - minY;\n }\n if (newMaxY != null && maxY !== newMaxY) {\n maxY = newMaxY;\n rangeY = maxY - minY;\n }\n }\n function bind (callback) {\n if (typeof callback === 'function') changeEvents.push(callback);\n }\n function unbind (callback) {\n if (typeof callback !== 'function') return;\n let i;\n while ((i = changeEvents.includes(callback))) changeEvents.splice(i, 1);\n }\n function destroy () {\n // unbind all possible events and null objects\n $(document).unbind('mouseup', mouseUp).unbind('mousemove', mouseMove);\n bar.unbind('mousedown', mouseDown);\n bar = null;\n arrow = null;\n changeEvents = null;\n }\n let offset,\n timeout,\n x = 0,\n y = 0,\n minX = 0,\n maxX = 100,\n rangeX = 100,\n minY = 0,\n maxY = 100,\n rangeY = 100,\n arrow = bar.find('img:first'), // the arrow image to drag\n changeEvents = [];\n\n $.extend(true, $this, // public properties, methods, and event bindings - these we need to access from other controls\n {\n val,\n range,\n bind,\n unbind,\n destroy\n }\n );\n // initialize this control\n arrow.src = options.arrow && options.arrow.image;\n arrow.w = (options.arrow && options.arrow.width) || arrow.width();\n arrow.h = (options.arrow && options.arrow.height) || arrow.height();\n bar.w = (options.map && options.map.width) || bar.width();\n bar.h = (options.map && options.map.height) || bar.height();\n // bind mousedown event\n bar.bind('mousedown', mouseDown);\n bind.call($this, draw);\n }\n // controls for all the input elements for the typing in color values\n function ColorValuePicker (picker, color, bindedHex, alphaPrecision) {\n const $this = this; // private properties and methods\n const inputs = picker.find('td.Text input');\n // input box key down - use arrows to alter color\n function keyDown (e) {\n if (e.target.value === '' && e.target !== hex.get(0) && ((bindedHex != null && e.target !== bindedHex.get(0)) || bindedHex == null)) return;\n if (!validateKey(e)) return e;\n switch (e.target) {\n case red.get(0):\n switch (e.keyCode) {\n case 38:\n red.val(setValueInRange.call($this, (red.val() << 0) + 1, 0, 255));\n color.val('r', red.val(), e.target);\n return false;\n case 40:\n red.val(setValueInRange.call($this, (red.val() << 0) - 1, 0, 255));\n color.val('r', red.val(), e.target);\n return false;\n }\n break;\n case green.get(0):\n switch (e.keyCode) {\n case 38:\n green.val(setValueInRange.call($this, (green.val() << 0) + 1, 0, 255));\n color.val('g', green.val(), e.target);\n return false;\n case 40:\n green.val(setValueInRange.call($this, (green.val() << 0) - 1, 0, 255));\n color.val('g', green.val(), e.target);\n return false;\n }\n break;\n case blue.get(0):\n switch (e.keyCode) {\n case 38:\n blue.val(setValueInRange.call($this, (blue.val() << 0) + 1, 0, 255));\n color.val('b', blue.val(), e.target);\n return false;\n case 40:\n blue.val(setValueInRange.call($this, (blue.val() << 0) - 1, 0, 255));\n color.val('b', blue.val(), e.target);\n return false;\n }\n break;\n case alpha && alpha.get(0):\n switch (e.keyCode) {\n case 38:\n alpha.val(setValueInRange.call($this, parseFloat(alpha.val()) + 1, 0, 100));\n color.val('a', Math.precision((alpha.val() * 255) / 100, alphaPrecision), e.target);\n return false;\n case 40:\n alpha.val(setValueInRange.call($this, parseFloat(alpha.val()) - 1, 0, 100));\n color.val('a', Math.precision((alpha.val() * 255) / 100, alphaPrecision), e.target);\n return false;\n }\n break;\n case hue.get(0):\n switch (e.keyCode) {\n case 38:\n hue.val(setValueInRange.call($this, (hue.val() << 0) + 1, 0, 360));\n color.val('h', hue.val(), e.target);\n return false;\n case 40:\n hue.val(setValueInRange.call($this, (hue.val() << 0) - 1, 0, 360));\n color.val('h', hue.val(), e.target);\n return false;\n }\n break;\n case saturation.get(0):\n switch (e.keyCode) {\n case 38:\n saturation.val(setValueInRange.call($this, (saturation.val() << 0) + 1, 0, 100));\n color.val('s', saturation.val(), e.target);\n return false;\n case 40:\n saturation.val(setValueInRange.call($this, (saturation.val() << 0) - 1, 0, 100));\n color.val('s', saturation.val(), e.target);\n return false;\n }\n break;\n case value.get(0):\n switch (e.keyCode) {\n case 38:\n value.val(setValueInRange.call($this, (value.val() << 0) + 1, 0, 100));\n color.val('v', value.val(), e.target);\n return false;\n case 40:\n value.val(setValueInRange.call($this, (value.val() << 0) - 1, 0, 100));\n color.val('v', value.val(), e.target);\n return false;\n }\n break;\n }\n }\n // input box key up - validate value and set color\n function keyUp (e) {\n if (e.target.value === '' && e.target !== hex.get(0) &&\n ((bindedHex != null && e.target !== bindedHex.get(0)) ||\n bindedHex == null)) return;\n if (!validateKey(e)) return e;\n switch (e.target) {\n case red.get(0):\n red.val(setValueInRange.call($this, red.val(), 0, 255));\n color.val('r', red.val(), e.target);\n break;\n case green.get(0):\n green.val(setValueInRange.call($this, green.val(), 0, 255));\n color.val('g', green.val(), e.target);\n break;\n case blue.get(0):\n blue.val(setValueInRange.call($this, blue.val(), 0, 255));\n color.val('b', blue.val(), e.target);\n break;\n case alpha && alpha.get(0):\n alpha.val(setValueInRange.call($this, alpha.val(), 0, 100));\n color.val('a', Math.precision((alpha.val() * 255) / 100, alphaPrecision), e.target);\n break;\n case hue.get(0):\n hue.val(setValueInRange.call($this, hue.val(), 0, 360));\n color.val('h', hue.val(), e.target);\n break;\n case saturation.get(0):\n saturation.val(setValueInRange.call($this, saturation.val(), 0, 100));\n color.val('s', saturation.val(), e.target);\n break;\n case value.get(0):\n value.val(setValueInRange.call($this, value.val(), 0, 100));\n color.val('v', value.val(), e.target);\n break;\n case hex.get(0):\n hex.val(hex.val().replace(/[^a-fA-F0-9]/g, '').toLowerCase().substring(0, 6));\n bindedHex && bindedHex.val(hex.val());\n color.val('hex', hex.val() !== '' ? hex.val() : null, e.target);\n break;\n case bindedHex && bindedHex.get(0):\n bindedHex.val(bindedHex.val().replace(/[^a-fA-F0-9]/g, '').toLowerCase().substring(0, 6));\n hex.val(bindedHex.val());\n color.val('hex', bindedHex.val() !== '' ? bindedHex.val() : null, e.target);\n break;\n case ahex && ahex.get(0):\n ahex.val(ahex.val().replace(/[^a-fA-F0-9]/g, '').toLowerCase().substring(0, 2));\n color.val('a', ahex.val() != null ? parseInt(ahex.val(), 16) : null, e.target);\n break;\n }\n }\n // input box blur - reset to original if value empty\n function blur (e) {\n if (color.val() != null) {\n switch (e.target) {\n case red.get(0): red.val(color.val('r')); break;\n case green.get(0): green.val(color.val('g')); break;\n case blue.get(0): blue.val(color.val('b')); break;\n case alpha && alpha.get(0): alpha.val(Math.precision((color.val('a') * 100) / 255, alphaPrecision)); break;\n case hue.get(0): hue.val(color.val('h')); break;\n case saturation.get(0): saturation.val(color.val('s')); break;\n case value.get(0): value.val(color.val('v')); break;\n case hex.get(0):\n case bindedHex && bindedHex.get(0):\n hex.val(color.val('hex'));\n bindedHex && bindedHex.val(color.val('hex'));\n break;\n case ahex && ahex.get(0): ahex.val(color.val('ahex').substring(6)); break;\n }\n }\n }\n function validateKey (e) {\n switch (e.keyCode) {\n case 9:\n case 16:\n case 29:\n case 37:\n case 39:\n return false;\n case 'c'.charCodeAt():\n case 'v'.charCodeAt():\n if (e.ctrlKey) return false;\n }\n return true;\n }\n // constrain value within range\n function setValueInRange (value, min, max) {\n if (value === '' || isNaN(value)) return min;\n if (value > max) return max;\n if (value < min) return min;\n return value;\n }\n function colorChanged (ui, context) {\n const all = ui.val('all');\n if (context !== red.get(0)) red.val(all != null ? all.r : '');\n if (context !== green.get(0)) green.val(all != null ? all.g : '');\n if (context !== blue.get(0)) blue.val(all != null ? all.b : '');\n if (alpha && context !== alpha.get(0)) alpha.val(all != null ? Math.precision((all.a * 100) / 255, alphaPrecision) : '');\n if (context !== hue.get(0)) hue.val(all != null ? all.h : '');\n if (context !== saturation.get(0)) saturation.val(all != null ? all.s : '');\n if (context !== value.get(0)) value.val(all != null ? all.v : '');\n if (context !== hex.get(0) && ((bindedHex && context !== bindedHex.get(0)) || !bindedHex)) hex.val(all != null ? all.hex : '');\n if (bindedHex && context !== bindedHex.get(0) && context !== hex.get(0)) bindedHex.val(all != null ? all.hex : '');\n if (ahex && context !== ahex.get(0)) ahex.val(all != null ? all.ahex.substring(6) : '');\n }\n function destroy () {\n // unbind all events and null objects\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).add(hex).add(bindedHex).add(ahex).unbind('keyup', keyUp).unbind('blur', blur);\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).unbind('keydown', keyDown);\n color.unbind(colorChanged);\n red = null;\n green = null;\n blue = null;\n alpha = null;\n hue = null;\n saturation = null;\n value = null;\n hex = null;\n ahex = null;\n }\n let\n red = inputs.eq(3),\n green = inputs.eq(4),\n blue = inputs.eq(5),\n alpha = inputs.length > 7 ? inputs.eq(6) : null,\n hue = inputs.eq(0),\n saturation = inputs.eq(1),\n value = inputs.eq(2),\n hex = inputs.eq(inputs.length > 7 ? 7 : 6),\n ahex = inputs.length > 7 ? inputs.eq(8) : null;\n $.extend(true, $this, {\n // public properties and methods\n destroy\n });\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).add(hex).add(bindedHex).add(ahex).bind('keyup', keyUp).bind('blur', blur);\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).bind('keydown', keyDown);\n color.bind(colorChanged);\n }\n\n $.jPicker = {\n List: [], // array holding references to each active instance of the control\n // color object - we will be able to assign by any color space type or retrieve any color space info\n // we want this public so we can optionally assign new color objects to initial values using inputs other than a string hex value (also supported)\n Color: class {\n constructor (init) {\n const $this = this;\n function fireChangeEvents (context) {\n for (let i = 0; i < changeEvents.length; i++) changeEvents[i].call($this, $this, context);\n }\n function val (name, value, context) {\n // Kind of ugly\n const set = Boolean(value);\n if (set && value.ahex === '') value.ahex = '00000000';\n if (!set) {\n if (name === undefined || name == null || name === '') name = 'all';\n if (r == null) return null;\n switch (name.toLowerCase()) {\n case 'ahex': return ColorMethods.rgbaToHex({ r, g, b, a });\n case 'hex': return val('ahex').substring(0, 6);\n case 'all': return { r, g, b, a, h, s, v, hex: val.call($this, 'hex'), ahex: val.call($this, 'ahex') };\n default:\n let ret = {};\n for (let i = 0; i < name.length; i++) {\n switch (name.charAt(i)) {\n case 'r':\n if (name.length === 1) ret = r;\n else ret.r = r;\n break;\n case 'g':\n if (name.length === 1) ret = g;\n else ret.g = g;\n break;\n case 'b':\n if (name.length === 1) ret = b;\n else ret.b = b;\n break;\n case 'a':\n if (name.length === 1) ret = a;\n else ret.a = a;\n break;\n case 'h':\n if (name.length === 1) ret = h;\n else ret.h = h;\n break;\n case 's':\n if (name.length === 1) ret = s;\n else ret.s = s;\n break;\n case 'v':\n if (name.length === 1) ret = v;\n else ret.v = v;\n break;\n }\n }\n return !name.length ? val.call($this, 'all') : ret;\n }\n }\n if (context != null && context === $this) return;\n if (name == null) name = '';\n\n let changed = false;\n if (value == null) {\n if (r != null) {\n r = null;\n changed = true;\n }\n if (g != null) {\n g = null;\n changed = true;\n }\n if (b != null) {\n b = null;\n changed = true;\n }\n if (a != null) {\n a = null;\n changed = true;\n }\n if (h != null) {\n h = null;\n changed = true;\n }\n if (s != null) {\n s = null;\n changed = true;\n }\n if (v != null) {\n v = null;\n changed = true;\n }\n changed && fireChangeEvents.call($this, context || $this);\n return;\n }\n switch (name.toLowerCase()) {\n case 'ahex':\n case 'hex':\n const ret = ColorMethods.hexToRgba((value && (value.ahex || value.hex)) || value || 'none');\n val.call($this, 'rgba', { r: ret.r, g: ret.g, b: ret.b, a: name === 'ahex' ? ret.a : a != null ? a : 255 }, context);\n break;\n default:\n if (value && (value.ahex != null || value.hex != null)) {\n val.call($this, 'ahex', value.ahex || value.hex || '00000000', context);\n return;\n }\n const newV = {};\n let rgb = false, hsv = false;\n if (value.r !== undefined && !name.includes('r')) name += 'r';\n if (value.g !== undefined && !name.includes('g')) name += 'g';\n if (value.b !== undefined && !name.includes('b')) name += 'b';\n if (value.a !== undefined && !name.includes('a')) name += 'a';\n if (value.h !== undefined && !name.includes('h')) name += 'h';\n if (value.s !== undefined && !name.includes('s')) name += 's';\n if (value.v !== undefined && !name.includes('v')) name += 'v';\n for (let i = 0; i < name.length; i++) {\n switch (name.charAt(i)) {\n case 'r':\n if (hsv) continue;\n rgb = true;\n newV.r = (value && value.r && value.r | 0) || (value && value | 0) || 0;\n if (newV.r < 0) newV.r = 0;\n else if (newV.r > 255) newV.r = 255;\n if (r !== newV.r) {\n ({r} = newV);\n changed = true;\n }\n break;\n case 'g':\n if (hsv) continue;\n rgb = true;\n newV.g = (value && value.g && value.g | 0) || (value && value | 0) || 0;\n if (newV.g < 0) newV.g = 0;\n else if (newV.g > 255) newV.g = 255;\n if (g !== newV.g) {\n ({g} = newV);\n changed = true;\n }\n break;\n case 'b':\n if (hsv) continue;\n rgb = true;\n newV.b = (value && value.b && value.b | 0) || (value && value | 0) || 0;\n if (newV.b < 0) newV.b = 0;\n else if (newV.b > 255) newV.b = 255;\n if (b !== newV.b) {\n ({b} = newV);\n changed = true;\n }\n break;\n case 'a':\n newV.a = value && value.a != null ? value.a | 0 : value != null ? value | 0 : 255;\n if (newV.a < 0) newV.a = 0;\n else if (newV.a > 255) newV.a = 255;\n if (a !== newV.a) {\n ({a} = newV);\n changed = true;\n }\n break;\n case 'h':\n if (rgb) continue;\n hsv = true;\n newV.h = (value && value.h && value.h | 0) || (value && value | 0) || 0;\n if (newV.h < 0) newV.h = 0;\n else if (newV.h > 360) newV.h = 360;\n if (h !== newV.h) {\n ({h} = newV);\n changed = true;\n }\n break;\n case 's':\n if (rgb) continue;\n hsv = true;\n newV.s = value && value.s != null ? value.s | 0 : value != null ? value | 0 : 100;\n if (newV.s < 0) newV.s = 0;\n else if (newV.s > 100) newV.s = 100;\n if (s !== newV.s) {\n ({s} = newV);\n changed = true;\n }\n break;\n case 'v':\n if (rgb) continue;\n hsv = true;\n newV.v = value && value.v != null ? value.v | 0 : value != null ? value | 0 : 100;\n if (newV.v < 0) newV.v = 0;\n else if (newV.v > 100) newV.v = 100;\n if (v !== newV.v) {\n ({v} = newV);\n changed = true;\n }\n break;\n }\n }\n if (changed) {\n if (rgb) {\n r = r || 0;\n g = g || 0;\n b = b || 0;\n const ret = ColorMethods.rgbToHsv({ r, g, b });\n ({h, s, v} = ret);\n } else if (hsv) {\n h = h || 0;\n s = s != null ? s : 100;\n v = v != null ? v : 100;\n const ret = ColorMethods.hsvToRgb({ h, s, v });\n ({r, g, b} = ret);\n }\n a = a != null ? a : 255;\n fireChangeEvents.call($this, context || $this);\n }\n break;\n }\n }\n function bind (callback) {\n if (typeof callback === 'function') changeEvents.push(callback);\n }\n function unbind (callback) {\n if (typeof callback !== 'function') return;\n let i;\n while ((i = changeEvents.includes(callback))) {\n changeEvents.splice(i, 1);\n }\n }\n function destroy () {\n changeEvents = null;\n }\n let r, g, b, a, h, s, v, changeEvents = [];\n\n $.extend(true, $this, {\n // public properties and methods\n val,\n bind,\n unbind,\n destroy\n });\n if (init) {\n if (init.ahex != null) {\n val('ahex', init);\n } else if (init.hex != null) {\n val(\n (init.a != null ? 'a' : '') + 'hex',\n init.a != null\n ? {ahex: init.hex + ColorMethods.intToHex(init.a)}\n : init\n );\n } else if (init.r != null && init.g != null && init.b != null) {\n val('rgb' + (init.a != null ? 'a' : ''), init);\n } else if (init.h != null && init.s != null && init.v != null) {\n val('hsv' + (init.a != null ? 'a' : ''), init);\n }\n }\n }\n },\n // color conversion methods - make public to give use to external scripts\n ColorMethods: {\n hexToRgba (hex) {\n if (hex === '' || hex === 'none') return { r: null, g: null, b: null, a: null };\n hex = this.validateHex(hex);\n let r = '00', g = '00', b = '00', a = '255';\n if (hex.length === 6) hex += 'ff';\n if (hex.length > 6) {\n r = hex.substring(0, 2);\n g = hex.substring(2, 4);\n b = hex.substring(4, 6);\n a = hex.substring(6, hex.length);\n } else {\n if (hex.length > 4) {\n r = hex.substring(4, hex.length);\n hex = hex.substring(0, 4);\n }\n if (hex.length > 2) {\n g = hex.substring(2, hex.length);\n hex = hex.substring(0, 2);\n }\n if (hex.length > 0) b = hex.substring(0, hex.length);\n }\n return { r: this.hexToInt(r), g: this.hexToInt(g), b: this.hexToInt(b), a: this.hexToInt(a) };\n },\n validateHex (hex) {\n // if (typeof hex === \"object\") return \"\";\n hex = hex.toLowerCase().replace(/[^a-f0-9]/g, '');\n if (hex.length > 8) hex = hex.substring(0, 8);\n return hex;\n },\n rgbaToHex (rgba) {\n return this.intToHex(rgba.r) + this.intToHex(rgba.g) + this.intToHex(rgba.b) + this.intToHex(rgba.a);\n },\n intToHex (dec) {\n let result = (dec | 0).toString(16);\n if (result.length === 1) result = ('0' + result);\n return result.toLowerCase();\n },\n hexToInt (hex) {\n return parseInt(hex, 16);\n },\n rgbToHsv (rgb) {\n const r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255, hsv = { h: 0, s: 0, v: 0 };\n let min = 0, max = 0;\n if (r >= g && r >= b) {\n max = r;\n min = g > b ? b : g;\n } else if (g >= b && g >= r) {\n max = g;\n min = r > b ? b : r;\n } else {\n max = b;\n min = g > r ? r : g;\n }\n hsv.v = max;\n hsv.s = max ? (max - min) / max : 0;\n let delta;\n if (!hsv.s) hsv.h = 0;\n else {\n delta = max - min;\n if (r === max) hsv.h = (g - b) / delta;\n else if (g === max) hsv.h = 2 + (b - r) / delta;\n else hsv.h = 4 + (r - g) / delta;\n hsv.h = parseInt(hsv.h * 60);\n if (hsv.h < 0) hsv.h += 360;\n }\n hsv.s = (hsv.s * 100) | 0;\n hsv.v = (hsv.v * 100) | 0;\n return hsv;\n },\n hsvToRgb (hsv) {\n const rgb = {r: 0, g: 0, b: 0, a: 100};\n let {h, s, v} = hsv;\n if (s === 0) {\n if (v === 0) rgb.r = rgb.g = rgb.b = 0;\n else rgb.r = rgb.g = rgb.b = (v * 255 / 100) | 0;\n } else {\n if (h === 360) h = 0;\n h /= 60;\n s = s / 100;\n v = v / 100;\n const i = h | 0,\n f = h - i,\n p = v * (1 - s),\n q = v * (1 - (s * f)),\n t = v * (1 - (s * (1 - f)));\n switch (i) {\n case 0:\n rgb.r = v;\n rgb.g = t;\n rgb.b = p;\n break;\n case 1:\n rgb.r = q;\n rgb.g = v;\n rgb.b = p;\n break;\n case 2:\n rgb.r = p;\n rgb.g = v;\n rgb.b = t;\n break;\n case 3:\n rgb.r = p;\n rgb.g = q;\n rgb.b = v;\n break;\n case 4:\n rgb.r = t;\n rgb.g = p;\n rgb.b = v;\n break;\n case 5:\n rgb.r = v;\n rgb.g = p;\n rgb.b = q;\n break;\n }\n rgb.r = (rgb.r * 255) | 0;\n rgb.g = (rgb.g * 255) | 0;\n rgb.b = (rgb.b * 255) | 0;\n }\n return rgb;\n }\n }\n };\n const {Color, List, ColorMethods} = $.jPicker; // local copies for YUI compressor\n $.fn.jPicker = function (options) {\n const $arguments = arguments;\n return this.each(function () {\n const $this = this, settings = $.extend(true, {}, $.fn.jPicker.defaults, options); // local copies for YUI compressor\n if ($($this).get(0).nodeName.toLowerCase() === 'input') { // Add color picker icon if binding to an input element and bind the events to the input\n $.extend(true, settings, {\n window: {\n bindToInput: true,\n expandable: true,\n input: $($this)\n }\n });\n if ($($this).val() === '') {\n settings.color.active = new Color({ hex: null });\n settings.color.current = new Color({ hex: null });\n } else if (ColorMethods.validateHex($($this).val())) {\n settings.color.active = new Color({ hex: $($this).val(), a: settings.color.active.val('a') });\n settings.color.current = new Color({ hex: $($this).val(), a: settings.color.active.val('a') });\n }\n }\n if (settings.window.expandable) {\n $($this).after('    ');\n } else {\n settings.window.liveUpdate = false; // Basic control binding for inline use - You will need to override the liveCallback or commitCallback function to retrieve results\n }\n const isLessThanIE7 = parseFloat(navigator.appVersion.split('MSIE')[1]) < 7 && document.body.filters; // needed to run the AlphaImageLoader function for IE6\n // set color mode and update visuals for the new color mode\n function setColorMode (colorMode) {\n const {active} = color, // local copies for YUI compressor\n // {clientPath} = images,\n hex = active.val('hex');\n let rgbMap, rgbBar;\n settings.color.mode = colorMode;\n switch (colorMode) {\n case 'h':\n setTimeout(function () {\n setBG.call($this, colorMapDiv, 'transparent');\n setImgLoc.call($this, colorMapL1, 0);\n setAlpha.call($this, colorMapL1, 100);\n setImgLoc.call($this, colorMapL2, 260);\n setAlpha.call($this, colorMapL2, 100);\n setBG.call($this, colorBarDiv, 'transparent');\n setImgLoc.call($this, colorBarL1, 0);\n setAlpha.call($this, colorBarL1, 100);\n setImgLoc.call($this, colorBarL2, 260);\n setAlpha.call($this, colorBarL2, 100);\n setImgLoc.call($this, colorBarL3, 260);\n setAlpha.call($this, colorBarL3, 100);\n setImgLoc.call($this, colorBarL4, 260);\n setAlpha.call($this, colorBarL4, 100);\n setImgLoc.call($this, colorBarL6, 260);\n setAlpha.call($this, colorBarL6, 100);\n }, 0);\n colorMap.range('all', { minX: 0, maxX: 100, minY: 0, maxY: 100 });\n colorBar.range('rangeY', { minY: 0, maxY: 360 });\n if (active.val('ahex') == null) break;\n colorMap.val('xy', { x: active.val('s'), y: 100 - active.val('v') }, colorMap);\n colorBar.val('y', 360 - active.val('h'), colorBar);\n break;\n case 's':\n setTimeout(function () {\n setBG.call($this, colorMapDiv, 'transparent');\n setImgLoc.call($this, colorMapL1, -260);\n setImgLoc.call($this, colorMapL2, -520);\n setImgLoc.call($this, colorBarL1, -260);\n setImgLoc.call($this, colorBarL2, -520);\n setImgLoc.call($this, colorBarL6, 260);\n setAlpha.call($this, colorBarL6, 100);\n }, 0);\n colorMap.range('all', { minX: 0, maxX: 360, minY: 0, maxY: 100 });\n colorBar.range('rangeY', { minY: 0, maxY: 100 });\n if (active.val('ahex') == null) break;\n colorMap.val('xy', { x: active.val('h'), y: 100 - active.val('v') }, colorMap);\n colorBar.val('y', 100 - active.val('s'), colorBar);\n break;\n case 'v':\n setTimeout(function () {\n setBG.call($this, colorMapDiv, '000000');\n setImgLoc.call($this, colorMapL1, -780);\n setImgLoc.call($this, colorMapL2, 260);\n setBG.call($this, colorBarDiv, hex);\n setImgLoc.call($this, colorBarL1, -520);\n setImgLoc.call($this, colorBarL2, 260);\n setAlpha.call($this, colorBarL2, 100);\n setImgLoc.call($this, colorBarL6, 260);\n setAlpha.call($this, colorBarL6, 100);\n }, 0);\n colorMap.range('all', { minX: 0, maxX: 360, minY: 0, maxY: 100 });\n colorBar.range('rangeY', { minY: 0, maxY: 100 });\n if (active.val('ahex') == null) break;\n colorMap.val('xy', { x: active.val('h'), y: 100 - active.val('s') }, colorMap);\n colorBar.val('y', 100 - active.val('v'), colorBar);\n break;\n case 'r':\n rgbMap = -1040;\n rgbBar = -780;\n colorMap.range('all', { minX: 0, maxX: 255, minY: 0, maxY: 255 });\n colorBar.range('rangeY', { minY: 0, maxY: 255 });\n if (active.val('ahex') == null) break;\n colorMap.val('xy', { x: active.val('b'), y: 255 - active.val('g') }, colorMap);\n colorBar.val('y', 255 - active.val('r'), colorBar);\n break;\n case 'g':\n rgbMap = -1560;\n rgbBar = -1820;\n colorMap.range('all', { minX: 0, maxX: 255, minY: 0, maxY: 255 });\n colorBar.range('rangeY', { minY: 0, maxY: 255 });\n if (active.val('ahex') == null) break;\n colorMap.val('xy', { x: active.val('b'), y: 255 - active.val('r') }, colorMap);\n colorBar.val('y', 255 - active.val('g'), colorBar);\n break;\n case 'b':\n rgbMap = -2080;\n rgbBar = -2860;\n colorMap.range('all', { minX: 0, maxX: 255, minY: 0, maxY: 255 });\n colorBar.range('rangeY', { minY: 0, maxY: 255 });\n if (active.val('ahex') == null) break;\n colorMap.val('xy', { x: active.val('r'), y: 255 - active.val('g') }, colorMap);\n colorBar.val('y', 255 - active.val('b'), colorBar);\n break;\n case 'a':\n setTimeout(function () {\n setBG.call($this, colorMapDiv, 'transparent');\n setImgLoc.call($this, colorMapL1, -260);\n setImgLoc.call($this, colorMapL2, -520);\n setImgLoc.call($this, colorBarL1, 260);\n setImgLoc.call($this, colorBarL2, 260);\n setAlpha.call($this, colorBarL2, 100);\n setImgLoc.call($this, colorBarL6, 0);\n setAlpha.call($this, colorBarL6, 100);\n }, 0);\n colorMap.range('all', { minX: 0, maxX: 360, minY: 0, maxY: 100 });\n colorBar.range('rangeY', { minY: 0, maxY: 255 });\n if (active.val('ahex') == null) break;\n colorMap.val('xy', { x: active.val('h'), y: 100 - active.val('v') }, colorMap);\n colorBar.val('y', 255 - active.val('a'), colorBar);\n break;\n default:\n throw new Error('Invalid Mode');\n }\n switch (colorMode) {\n case 'h':\n break;\n case 's':\n case 'v':\n case 'a':\n setTimeout(function () {\n setAlpha.call($this, colorMapL1, 100);\n setAlpha.call($this, colorBarL1, 100);\n setImgLoc.call($this, colorBarL3, 260);\n setAlpha.call($this, colorBarL3, 100);\n setImgLoc.call($this, colorBarL4, 260);\n setAlpha.call($this, colorBarL4, 100);\n }, 0);\n break;\n case 'r':\n case 'g':\n case 'b':\n setTimeout(function () {\n setBG.call($this, colorMapDiv, 'transparent');\n setBG.call($this, colorBarDiv, 'transparent');\n setAlpha.call($this, colorBarL1, 100);\n setAlpha.call($this, colorMapL1, 100);\n setImgLoc.call($this, colorMapL1, rgbMap);\n setImgLoc.call($this, colorMapL2, rgbMap - 260);\n setImgLoc.call($this, colorBarL1, rgbBar - 780);\n setImgLoc.call($this, colorBarL2, rgbBar - 520);\n setImgLoc.call($this, colorBarL3, rgbBar);\n setImgLoc.call($this, colorBarL4, rgbBar - 260);\n setImgLoc.call($this, colorBarL6, 260);\n setAlpha.call($this, colorBarL6, 100);\n }, 0);\n break;\n }\n if (active.val('ahex') == null) return;\n activeColorChanged.call($this, active);\n }\n // Update color when user changes text values\n function activeColorChanged (ui, context) {\n if (context == null || (context !== colorBar && context !== colorMap)) positionMapAndBarArrows.call($this, ui, context);\n setTimeout(function () {\n updatePreview.call($this, ui);\n updateMapVisuals.call($this, ui);\n updateBarVisuals.call($this, ui);\n }, 0);\n }\n // user has dragged the ColorMap pointer\n function mapValueChanged (ui, context) {\n const {active} = color;\n if (context !== colorMap && active.val() == null) return;\n const xy = ui.val('all');\n switch (settings.color.mode) {\n case 'h':\n active.val('sv', { s: xy.x, v: 100 - xy.y }, context);\n break;\n case 's':\n case 'a':\n active.val('hv', { h: xy.x, v: 100 - xy.y }, context);\n break;\n case 'v':\n active.val('hs', { h: xy.x, s: 100 - xy.y }, context);\n break;\n case 'r':\n active.val('gb', { g: 255 - xy.y, b: xy.x }, context);\n break;\n case 'g':\n active.val('rb', { r: 255 - xy.y, b: xy.x }, context);\n break;\n case 'b':\n active.val('rg', { r: xy.x, g: 255 - xy.y }, context);\n break;\n }\n }\n // user has dragged the ColorBar slider\n function colorBarValueChanged (ui, context) {\n const {active} = color;\n if (context !== colorBar && active.val() == null) return;\n switch (settings.color.mode) {\n case 'h':\n active.val('h', { h: 360 - ui.val('y') }, context);\n break;\n case 's':\n active.val('s', { s: 100 - ui.val('y') }, context);\n break;\n case 'v':\n active.val('v', { v: 100 - ui.val('y') }, context);\n break;\n case 'r':\n active.val('r', { r: 255 - ui.val('y') }, context);\n break;\n case 'g':\n active.val('g', { g: 255 - ui.val('y') }, context);\n break;\n case 'b':\n active.val('b', { b: 255 - ui.val('y') }, context);\n break;\n case 'a':\n active.val('a', 255 - ui.val('y'), context);\n break;\n }\n }\n // position map and bar arrows to match current color\n function positionMapAndBarArrows (ui, context) {\n if (context !== colorMap) {\n switch (settings.color.mode) {\n case 'h':\n const sv = ui.val('sv');\n colorMap.val('xy', { x: sv != null ? sv.s : 100, y: 100 - (sv != null ? sv.v : 100) }, context);\n break;\n case 's':\n case 'a':\n const hv = ui.val('hv');\n colorMap.val('xy', { x: (hv && hv.h) || 0, y: 100 - (hv != null ? hv.v : 100) }, context);\n break;\n case 'v':\n const hs = ui.val('hs');\n colorMap.val('xy', { x: (hs && hs.h) || 0, y: 100 - (hs != null ? hs.s : 100) }, context);\n break;\n case 'r':\n const bg = ui.val('bg');\n colorMap.val('xy', { x: (bg && bg.b) || 0, y: 255 - ((bg && bg.g) || 0) }, context);\n break;\n case 'g':\n const br = ui.val('br');\n colorMap.val('xy', { x: (br && br.b) || 0, y: 255 - ((br && br.r) || 0) }, context);\n break;\n case 'b':\n const rg = ui.val('rg');\n colorMap.val('xy', { x: (rg && rg.r) || 0, y: 255 - ((rg && rg.g) || 0) }, context);\n break;\n }\n }\n if (context !== colorBar) {\n switch (settings.color.mode) {\n case 'h':\n colorBar.val('y', 360 - (ui.val('h') || 0), context);\n break;\n case 's':\n const s = ui.val('s');\n colorBar.val('y', 100 - (s != null ? s : 100), context);\n break;\n case 'v':\n const v = ui.val('v');\n colorBar.val('y', 100 - (v != null ? v : 100), context);\n break;\n case 'r':\n colorBar.val('y', 255 - (ui.val('r') || 0), context);\n break;\n case 'g':\n colorBar.val('y', 255 - (ui.val('g') || 0), context);\n break;\n case 'b':\n colorBar.val('y', 255 - (ui.val('b') || 0), context);\n break;\n case 'a':\n const a = ui.val('a');\n colorBar.val('y', 255 - (a != null ? a : 255), context);\n break;\n }\n }\n }\n function updatePreview (ui) {\n try {\n const all = ui.val('all');\n activePreview.css({ backgroundColor: (all && '#' + all.hex) || 'transparent' });\n setAlpha.call($this, activePreview, (all && Math.precision((all.a * 100) / 255, 4)) || 0);\n } catch (e) { }\n }\n function updateMapVisuals (ui) {\n switch (settings.color.mode) {\n case 'h':\n setBG.call($this, colorMapDiv, new Color({ h: ui.val('h') || 0, s: 100, v: 100 }).val('hex'));\n break;\n case 's':\n case 'a':\n const s = ui.val('s');\n setAlpha.call($this, colorMapL2, 100 - (s != null ? s : 100));\n break;\n case 'v':\n const v = ui.val('v');\n setAlpha.call($this, colorMapL1, v != null ? v : 100);\n break;\n case 'r':\n setAlpha.call($this, colorMapL2, Math.precision((ui.val('r') || 0) / 255 * 100, 4));\n break;\n case 'g':\n setAlpha.call($this, colorMapL2, Math.precision((ui.val('g') || 0) / 255 * 100, 4));\n break;\n case 'b':\n setAlpha.call($this, colorMapL2, Math.precision((ui.val('b') || 0) / 255 * 100));\n break;\n }\n const a = ui.val('a');\n setAlpha.call($this, colorMapL3, Math.precision(((255 - (a || 0)) * 100) / 255, 4));\n }\n function updateBarVisuals (ui) {\n switch (settings.color.mode) {\n case 'h':\n const a = ui.val('a');\n setAlpha.call($this, colorBarL5, Math.precision(((255 - (a || 0)) * 100) / 255, 4));\n break;\n case 's':\n const hva = ui.val('hva'),\n saturatedColor = new Color({ h: (hva && hva.h) || 0, s: 100, v: hva != null ? hva.v : 100 });\n setBG.call($this, colorBarDiv, saturatedColor.val('hex'));\n setAlpha.call($this, colorBarL2, 100 - (hva != null ? hva.v : 100));\n setAlpha.call($this, colorBarL5, Math.precision(((255 - ((hva && hva.a) || 0)) * 100) / 255, 4));\n break;\n case 'v':\n const hsa = ui.val('hsa'),\n valueColor = new Color({ h: (hsa && hsa.h) || 0, s: hsa != null ? hsa.s : 100, v: 100 });\n setBG.call($this, colorBarDiv, valueColor.val('hex'));\n setAlpha.call($this, colorBarL5, Math.precision(((255 - ((hsa && hsa.a) || 0)) * 100) / 255, 4));\n break;\n case 'r':\n case 'g':\n case 'b':\n const rgba = ui.val('rgba');\n let hValue = 0, vValue = 0;\n if (settings.color.mode === 'r') {\n hValue = (rgba && rgba.b) || 0;\n vValue = (rgba && rgba.g) || 0;\n } else if (settings.color.mode === 'g') {\n hValue = (rgba && rgba.b) || 0;\n vValue = (rgba && rgba.r) || 0;\n } else if (settings.color.mode === 'b') {\n hValue = (rgba && rgba.r) || 0;\n vValue = (rgba && rgba.g) || 0;\n }\n const middle = vValue > hValue ? hValue : vValue;\n setAlpha.call($this, colorBarL2, hValue > vValue ? Math.precision(((hValue - vValue) / (255 - vValue)) * 100, 4) : 0);\n setAlpha.call($this, colorBarL3, vValue > hValue ? Math.precision(((vValue - hValue) / (255 - hValue)) * 100, 4) : 0);\n setAlpha.call($this, colorBarL4, Math.precision((middle / 255) * 100, 4));\n setAlpha.call($this, colorBarL5, Math.precision(((255 - ((rgba && rgba.a) || 0)) * 100) / 255, 4));\n break;\n case 'a': {\n const a = ui.val('a');\n setBG.call($this, colorBarDiv, ui.val('hex') || '000000');\n setAlpha.call($this, colorBarL5, a != null ? 0 : 100);\n setAlpha.call($this, colorBarL6, a != null ? 100 : 0);\n break;\n }\n }\n }\n function setBG (el, c) {\n el.css({backgroundColor: (c && c.length === 6 && '#' + c) || 'transparent'});\n }\n function setImg (img, src) {\n if (isLessThanIE7 && (src.includes('AlphaBar.png') || src.includes('Bars.png') || src.includes('Maps.png'))) {\n img.attr('pngSrc', src);\n img.css({ backgroundImage: 'none', filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\\'' + src + '\\', sizingMethod=\\'scale\\')' });\n } else img.css({ backgroundImage: 'url(\\'' + src + '\\')' });\n }\n function setImgLoc (img, y) {\n img.css({ top: y + 'px' });\n }\n function setAlpha (obj, alpha) {\n obj.css({ visibility: alpha > 0 ? 'visible' : 'hidden' });\n if (alpha > 0 && alpha < 100) {\n if (isLessThanIE7) {\n const src = obj.attr('pngSrc');\n if (src != null && (\n src.includes('AlphaBar.png') || src.includes('Bars.png') || src.includes('Maps.png')\n )) {\n obj.css({ filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\\'' + src + '\\', sizingMethod=\\'scale\\') progid:DXImageTransform.Microsoft.Alpha(opacity=' + alpha + ')' });\n } else obj.css({ opacity: Math.precision(alpha / 100, 4) });\n } else obj.css({ opacity: Math.precision(alpha / 100, 4) });\n } else if (alpha === 0 || alpha === 100) {\n if (isLessThanIE7) {\n const src = obj.attr('pngSrc');\n if (src != null && (\n src.includes('AlphaBar.png') || src.includes('Bars.png') || src.includes('Maps.png')\n )) {\n obj.css({ filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\\'' + src + '\\', sizingMethod=\\'scale\\')' });\n } else obj.css({ opacity: '' });\n } else obj.css({ opacity: '' });\n }\n }\n // revert color to original color when opened\n function revertColor () {\n color.active.val('ahex', color.current.val('ahex'));\n }\n // commit the color changes\n function commitColor () {\n color.current.val('ahex', color.active.val('ahex'));\n }\n function radioClicked (e) {\n $(this).parents('tbody:first').find('input:radio[value!=\"' + e.target.value + '\"]').removeAttr('checked');\n setColorMode.call($this, e.target.value);\n }\n function currentClicked () {\n revertColor.call($this);\n }\n function cancelClicked () {\n revertColor.call($this);\n settings.window.expandable && hide.call($this);\n typeof cancelCallback === 'function' && cancelCallback.call($this, color.active, cancelButton);\n }\n function okClicked () {\n commitColor.call($this);\n settings.window.expandable && hide.call($this);\n typeof commitCallback === 'function' && commitCallback.call($this, color.active, okButton);\n }\n function iconImageClicked () {\n show.call($this);\n }\n function currentColorChanged (ui, context) {\n const hex = ui.val('hex');\n currentPreview.css({ backgroundColor: (hex && '#' + hex) || 'transparent' });\n setAlpha.call($this, currentPreview, Math.precision(((ui.val('a') || 0) * 100) / 255, 4));\n }\n function expandableColorChanged (ui, context) {\n const hex = ui.val('hex');\n const va = ui.val('va');\n iconColor.css({ backgroundColor: (hex && '#' + hex) || 'transparent' });\n setAlpha.call($this, iconAlpha, Math.precision(((255 - ((va && va.a) || 0)) * 100) / 255, 4));\n if (settings.window.bindToInput && settings.window.updateInputColor) {\n settings.window.input.css({\n backgroundColor: (hex && '#' + hex) || 'transparent',\n color: va == null || va.v > 75 ? '#000000' : '#ffffff'\n });\n }\n }\n function moveBarMouseDown (e) {\n // const {element} = settings.window, // local copies for YUI compressor\n // {page} = settings.window;\n elementStartX = parseInt(container.css('left'));\n elementStartY = parseInt(container.css('top'));\n pageStartX = e.pageX;\n pageStartY = e.pageY;\n // bind events to document to move window - we will unbind these on mouseup\n $(document).bind('mousemove', documentMouseMove).bind('mouseup', documentMouseUp);\n e.preventDefault(); // prevent attempted dragging of the column\n }\n function documentMouseMove (e) {\n container.css({ left: elementStartX - (pageStartX - e.pageX) + 'px', top: elementStartY - (pageStartY - e.pageY) + 'px' });\n if (settings.window.expandable && !$.support.boxModel) container.prev().css({ left: container.css('left'), top: container.css('top') });\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n function documentMouseUp (e) {\n $(document).unbind('mousemove', documentMouseMove).unbind('mouseup', documentMouseUp);\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n function quickPickClicked (e) {\n e.preventDefault();\n e.stopPropagation();\n color.active.val('ahex', $(this).attr('title') || null, e.target);\n return false;\n }\n function show () {\n color.current.val('ahex', color.active.val('ahex'));\n function attachIFrame () {\n if (!settings.window.expandable || $.support.boxModel) return;\n const table = container.find('table:first');\n container.before('