move files to adequate folders and fix imports
285
src/common/browser.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/* globals jQuery */
|
||||
/**
|
||||
* Browser detection.
|
||||
* @module browser
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Jeff Schiller, 2010 Alexis Deveria
|
||||
*/
|
||||
|
||||
// Dependencies:
|
||||
// 1) jQuery (for $.alert())
|
||||
|
||||
import './svgpathseg.js';
|
||||
import {NS} from './namespaces.js';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
const supportsSVG_ = (function () {
|
||||
return Boolean(document.createElementNS && document.createElementNS(NS.SVG, 'svg').createSVGRect);
|
||||
}());
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsSvg
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsSvg = () => supportsSVG_;
|
||||
|
||||
const {userAgent} = navigator;
|
||||
const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
|
||||
// Note: Browser sniffing should only be used if no other detection method is possible
|
||||
const isOpera_ = Boolean(window.opera);
|
||||
const isWebkit_ = userAgent.includes('AppleWebKit');
|
||||
const isGecko_ = userAgent.includes('Gecko/');
|
||||
const isIE_ = userAgent.includes('MSIE');
|
||||
const isChrome_ = userAgent.includes('Chrome/');
|
||||
const isWindows_ = userAgent.includes('Windows');
|
||||
const isMac_ = userAgent.includes('Macintosh');
|
||||
const isTouch_ = 'ontouchstart' in window;
|
||||
|
||||
const supportsSelectors_ = (function () {
|
||||
return Boolean(svg.querySelector);
|
||||
}());
|
||||
|
||||
const supportsXpath_ = (function () {
|
||||
return Boolean(document.evaluate);
|
||||
}());
|
||||
|
||||
// segList functions (for FF1.5 and 2.0)
|
||||
const supportsPathReplaceItem_ = (function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 10,10');
|
||||
const seglist = path.pathSegList;
|
||||
const seg = path.createSVGPathSegLinetoAbs(5, 5);
|
||||
try {
|
||||
seglist.replaceItem(seg, 1);
|
||||
return true;
|
||||
} catch (err) {}
|
||||
return false;
|
||||
}());
|
||||
|
||||
const supportsPathInsertItemBefore_ = (function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 10,10');
|
||||
const seglist = path.pathSegList;
|
||||
const seg = path.createSVGPathSegLinetoAbs(5, 5);
|
||||
try {
|
||||
seglist.insertItemBefore(seg, 1);
|
||||
return true;
|
||||
} catch (err) {}
|
||||
return false;
|
||||
}());
|
||||
|
||||
// text character positioning (for IE9 and now Chrome)
|
||||
const supportsGoodTextCharPos_ = (function () {
|
||||
const svgroot = document.createElementNS(NS.SVG, 'svg');
|
||||
const svgcontent = document.createElementNS(NS.SVG, 'svg');
|
||||
document.documentElement.append(svgroot);
|
||||
svgcontent.setAttribute('x', 5);
|
||||
svgroot.append(svgcontent);
|
||||
const text = document.createElementNS(NS.SVG, 'text');
|
||||
text.textContent = 'a';
|
||||
svgcontent.append(text);
|
||||
try { // Chrome now fails here
|
||||
const pos = text.getStartPositionOfChar(0).x;
|
||||
return (pos === 0);
|
||||
} catch (err) {
|
||||
return false;
|
||||
} finally {
|
||||
svgroot.remove();
|
||||
}
|
||||
}());
|
||||
|
||||
const supportsPathBBox_ = (function () {
|
||||
const svgcontent = document.createElementNS(NS.SVG, 'svg');
|
||||
document.documentElement.append(svgcontent);
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 C0,0 10,10 10,0');
|
||||
svgcontent.append(path);
|
||||
const bbox = path.getBBox();
|
||||
svgcontent.remove();
|
||||
return (bbox.height > 4 && bbox.height < 5);
|
||||
}());
|
||||
|
||||
// Support for correct bbox sizing on groups with horizontal/vertical lines
|
||||
const supportsHVLineContainerBBox_ = (function () {
|
||||
const svgcontent = document.createElementNS(NS.SVG, 'svg');
|
||||
document.documentElement.append(svgcontent);
|
||||
const path = document.createElementNS(NS.SVG, 'path');
|
||||
path.setAttribute('d', 'M0,0 10,0');
|
||||
const path2 = document.createElementNS(NS.SVG, 'path');
|
||||
path2.setAttribute('d', 'M5,0 15,0');
|
||||
const g = document.createElementNS(NS.SVG, 'g');
|
||||
g.append(path, path2);
|
||||
svgcontent.append(g);
|
||||
const bbox = g.getBBox();
|
||||
svgcontent.remove();
|
||||
// Webkit gives 0, FF gives 10, Opera (correctly) gives 15
|
||||
return (bbox.width === 15);
|
||||
}());
|
||||
|
||||
const supportsEditableText_ = (function () {
|
||||
// TODO: Find better way to check support for this
|
||||
return isOpera_;
|
||||
}());
|
||||
|
||||
const supportsGoodDecimals_ = (function () {
|
||||
// Correct decimals on clone attributes (Opera < 10.5/win/non-en)
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
rect.setAttribute('x', 0.1);
|
||||
const crect = rect.cloneNode(false);
|
||||
const retValue = (!crect.getAttribute('x').includes(','));
|
||||
if (!retValue) {
|
||||
// Todo: i18nize or remove
|
||||
$.alert(
|
||||
'NOTE: This version of Opera is known to contain bugs in SVG-edit.\n' +
|
||||
'Please upgrade to the <a href="http://opera.com">latest version</a> in which the problems have been fixed.'
|
||||
);
|
||||
}
|
||||
return retValue;
|
||||
}());
|
||||
|
||||
const supportsNonScalingStroke_ = (function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
rect.setAttribute('style', 'vector-effect:non-scaling-stroke');
|
||||
return rect.style.vectorEffect === 'non-scaling-stroke';
|
||||
}());
|
||||
|
||||
let supportsNativeSVGTransformLists_ = (function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
const rxform = rect.transform.baseVal;
|
||||
const t1 = svg.createSVGTransform();
|
||||
rxform.appendItem(t1);
|
||||
const r1 = rxform.getItem(0);
|
||||
const isSVGTransform = (o) => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SVGTransform
|
||||
return o && typeof o === 'object' && typeof o.setMatrix === 'function' && 'angle' in o;
|
||||
};
|
||||
return isSVGTransform(r1) && isSVGTransform(t1) &&
|
||||
r1.type === t1.type && r1.angle === t1.angle &&
|
||||
r1.matrix.a === t1.matrix.a &&
|
||||
r1.matrix.b === t1.matrix.b &&
|
||||
r1.matrix.c === t1.matrix.c &&
|
||||
r1.matrix.d === t1.matrix.d &&
|
||||
r1.matrix.e === t1.matrix.e &&
|
||||
r1.matrix.f === t1.matrix.f;
|
||||
}());
|
||||
|
||||
// Public API
|
||||
|
||||
/**
|
||||
* @function module:browser.isOpera
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isOpera = () => isOpera_;
|
||||
/**
|
||||
* @function module:browser.isWebkit
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWebkit = () => isWebkit_;
|
||||
/**
|
||||
* @function module:browser.isGecko
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isGecko = () => isGecko_;
|
||||
/**
|
||||
* @function module:browser.isIE
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isIE = () => isIE_;
|
||||
/**
|
||||
* @function module:browser.isChrome
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isChrome = () => isChrome_;
|
||||
/**
|
||||
* @function module:browser.isWindows
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWindows = () => isWindows_;
|
||||
/**
|
||||
* @function module:browser.isMac
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isMac = () => isMac_;
|
||||
/**
|
||||
* @function module:browser.isTouch
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isTouch = () => isTouch_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsSelectors
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsSelectors = () => supportsSelectors_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsXpath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsXpath = () => supportsXpath_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsPathReplaceItem
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsPathReplaceItem = () => supportsPathReplaceItem_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsPathInsertItemBefore
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsPathInsertItemBefore = () => supportsPathInsertItemBefore_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsPathBBox
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsPathBBox = () => supportsPathBBox_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsHVLineContainerBBox
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsHVLineContainerBBox = () => supportsHVLineContainerBBox_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsGoodTextCharPos
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsGoodTextCharPos = () => supportsGoodTextCharPos_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsEditableText
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsEditableText = () => supportsEditableText_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsGoodDecimals
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsGoodDecimals = () => supportsGoodDecimals_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsNonScalingStroke
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsNonScalingStroke = () => supportsNonScalingStroke_;
|
||||
|
||||
/**
|
||||
* @function module:browser.supportsNativeTransformLists
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const supportsNativeTransformLists = () => supportsNativeSVGTransformLists_;
|
||||
|
||||
/**
|
||||
* Set `supportsNativeSVGTransformLists_` to `false` (for unit testing).
|
||||
* @function module:browser.disableSupportsNativeTransformLists
|
||||
* @returns {void}
|
||||
*/
|
||||
export const disableSupportsNativeTransformLists = () => {
|
||||
supportsNativeSVGTransformLists_ = false;
|
||||
};
|
||||
79
src/common/jQuery.attr.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* A jQuery module to work with SVG attributes.
|
||||
* @module jQueryAttr
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* This fixes `$(...).attr()` to work as expected with SVG elements.
|
||||
* Does not currently use `*AttributeNS()` since we rarely need that.
|
||||
* Adds {@link external:jQuery.fn.attr}.
|
||||
* See {@link https://api.jquery.com/attr/} for basic documentation of `.attr()`.
|
||||
*
|
||||
* Additional functionality:
|
||||
* - When getting attributes, a string that's a number is returned as type number.
|
||||
* - If an array is supplied as the first parameter, multiple values are returned
|
||||
* as an object with values for each given attribute.
|
||||
* @function module:jQueryAttr.jQueryAttr
|
||||
* @param {external:jQuery} $ The jQuery object to which to add the plug-in
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
export default function jQueryPluginSVG ($) {
|
||||
const proxied = $.fn.attr,
|
||||
svgns = 'http://www.w3.org/2000/svg';
|
||||
/**
|
||||
* @typedef {PlainObject<string, string|Float>} module:jQueryAttr.Attributes
|
||||
*/
|
||||
/**
|
||||
* @function external:jQuery.fn.attr
|
||||
* @param {string|string[]|PlainObject<string, string>} key
|
||||
* @param {string} value
|
||||
* @returns {external:jQuery|module:jQueryAttr.Attributes}
|
||||
*/
|
||||
$.fn.attr = function (key, value) {
|
||||
const len = this.length;
|
||||
if (!len) { return proxied.call(this, key, value); }
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const elem = this[i];
|
||||
// set/get SVG attribute
|
||||
if (elem.namespaceURI === svgns) {
|
||||
// Setting attribute
|
||||
if (value !== undefined) {
|
||||
elem.setAttribute(key, value);
|
||||
} else if (Array.isArray(key)) {
|
||||
// Getting attributes from array
|
||||
const obj = {};
|
||||
let j = key.length;
|
||||
|
||||
while (j--) {
|
||||
const aname = key[j];
|
||||
let attr = elem.getAttribute(aname);
|
||||
// This returns a number when appropriate
|
||||
if (attr || attr === '0') {
|
||||
attr = isNaN(attr) ? attr : (attr - 0);
|
||||
}
|
||||
obj[aname] = attr;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (typeof key === 'object') {
|
||||
// Setting attributes from object
|
||||
for (const [name, val] of Object.entries(key)) {
|
||||
elem.setAttribute(name, val);
|
||||
}
|
||||
// Getting attribute
|
||||
} else {
|
||||
let attr = elem.getAttribute(key);
|
||||
if (attr || attr === '0') {
|
||||
attr = isNaN(attr) ? attr : (attr - 0);
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
} else {
|
||||
return proxied.call(this, key, value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
return $;
|
||||
}
|
||||
222
src/common/layer.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/* globals jQuery */
|
||||
/**
|
||||
* Provides tools for the layer concept.
|
||||
* @module layer
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2011 Jeff Schiller, 2016 Flint O'Brien
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
import {toXml, walkTree, isNullish} from './utilities.js';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
/**
|
||||
* This class encapsulates the concept of a layer in the drawing. It can be constructed with
|
||||
* an existing group element or, with three parameters, will create a new layer group element.
|
||||
*
|
||||
* @example
|
||||
* const l1 = new Layer('name', group); // Use the existing group for this layer.
|
||||
* const l2 = new Layer('name', group, svgElem); // Create a new group and add it to the DOM after group.
|
||||
* const l3 = new Layer('name', null, svgElem); // Create a new group and add it to the DOM as the last layer.
|
||||
* @memberof module:layer
|
||||
*/
|
||||
class Layer {
|
||||
/**
|
||||
* @param {string} name - Layer name
|
||||
* @param {SVGGElement|null} group - An existing SVG group element or null.
|
||||
* If group and no svgElem, use group for this layer.
|
||||
* If group and svgElem, create a new group element and insert it in the DOM after group.
|
||||
* If no group and svgElem, create a new group element and insert it in the DOM as the last layer.
|
||||
* @param {SVGGElement} [svgElem] - The SVG DOM element. If defined, use this to add
|
||||
* a new layer to the document.
|
||||
*/
|
||||
constructor (name, group, svgElem) {
|
||||
this.name_ = name;
|
||||
this.group_ = svgElem ? null : group;
|
||||
|
||||
if (svgElem) {
|
||||
// Create a group element with title and add it to the DOM.
|
||||
const svgdoc = svgElem.ownerDocument;
|
||||
this.group_ = svgdoc.createElementNS(NS.SVG, 'g');
|
||||
const layerTitle = svgdoc.createElementNS(NS.SVG, 'title');
|
||||
layerTitle.textContent = name;
|
||||
this.group_.append(layerTitle);
|
||||
if (group) {
|
||||
$(group).after(this.group_);
|
||||
} else {
|
||||
svgElem.append(this.group_);
|
||||
}
|
||||
}
|
||||
|
||||
addLayerClass(this.group_);
|
||||
walkTree(this.group_, function (e) {
|
||||
e.setAttribute('style', 'pointer-events:inherit');
|
||||
});
|
||||
|
||||
this.group_.setAttribute('style', svgElem ? 'pointer-events:all' : 'pointer-events:none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the layer's name.
|
||||
* @returns {string} The layer name
|
||||
*/
|
||||
getName () {
|
||||
return this.name_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the group element for this layer.
|
||||
* @returns {SVGGElement} The layer SVG group
|
||||
*/
|
||||
getGroup () {
|
||||
return this.group_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active this layer so it takes pointer events.
|
||||
* @returns {void}
|
||||
*/
|
||||
activate () {
|
||||
this.group_.setAttribute('style', 'pointer-events:all');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactive this layer so it does NOT take pointer events.
|
||||
* @returns {void}
|
||||
*/
|
||||
deactivate () {
|
||||
this.group_.setAttribute('style', 'pointer-events:none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this layer visible or hidden based on 'visible' parameter.
|
||||
* @param {boolean} visible - If true, make visible; otherwise, hide it.
|
||||
* @returns {void}
|
||||
*/
|
||||
setVisible (visible) {
|
||||
const expected = visible === undefined || visible ? 'inline' : 'none';
|
||||
const oldDisplay = this.group_.getAttribute('display');
|
||||
if (oldDisplay !== expected) {
|
||||
this.group_.setAttribute('display', expected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this layer visible?
|
||||
* @returns {boolean} True if visible.
|
||||
*/
|
||||
isVisible () {
|
||||
return this.group_.getAttribute('display') !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get layer opacity.
|
||||
* @returns {Float} Opacity value.
|
||||
*/
|
||||
getOpacity () {
|
||||
const opacity = this.group_.getAttribute('opacity');
|
||||
if (isNullish(opacity)) {
|
||||
return 1;
|
||||
}
|
||||
return Number.parseFloat(opacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the opacity of this layer. If opacity is not a value between 0.0 and 1.0,
|
||||
* nothing happens.
|
||||
* @param {Float} opacity - A float value in the range 0.0-1.0
|
||||
* @returns {void}
|
||||
*/
|
||||
setOpacity (opacity) {
|
||||
if (typeof opacity === 'number' && opacity >= 0.0 && opacity <= 1.0) {
|
||||
this.group_.setAttribute('opacity', opacity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append children to this layer.
|
||||
* @param {SVGGElement} children - The children to append to this layer.
|
||||
* @returns {void}
|
||||
*/
|
||||
appendChildren (children) {
|
||||
for (const child of children) {
|
||||
this.group_.append(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {SVGTitleElement|null}
|
||||
*/
|
||||
getTitleElement () {
|
||||
const len = this.group_.childNodes.length;
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const child = this.group_.childNodes.item(i);
|
||||
if (child && child.tagName === 'title') {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of this layer.
|
||||
* @param {string} name - The new name.
|
||||
* @param {module:history.HistoryRecordingService} hrService - History recording service
|
||||
* @returns {string|null} The new name if changed; otherwise, null.
|
||||
*/
|
||||
setName (name, hrService) {
|
||||
const previousName = this.name_;
|
||||
name = toXml(name);
|
||||
// now change the underlying title element contents
|
||||
const title = this.getTitleElement();
|
||||
if (title) {
|
||||
$(title).empty();
|
||||
title.textContent = name;
|
||||
this.name_ = name;
|
||||
if (hrService) {
|
||||
hrService.changeElement(title, {'#text': previousName});
|
||||
}
|
||||
return this.name_;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this layer's group from the DOM. No more functions on group can be called after this.
|
||||
* @returns {SVGGElement} The layer SVG group that was just removed.
|
||||
*/
|
||||
removeGroup () {
|
||||
const group = this.group_;
|
||||
this.group_.remove();
|
||||
this.group_ = undefined;
|
||||
return group;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @property {string} CLASS_NAME - class attribute assigned to all layer groups.
|
||||
*/
|
||||
Layer.CLASS_NAME = 'layer';
|
||||
|
||||
/**
|
||||
* @property {RegExp} CLASS_REGEX - Used to test presence of class Layer.CLASS_NAME
|
||||
*/
|
||||
Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)');
|
||||
|
||||
/**
|
||||
* Add class `Layer.CLASS_NAME` to the element (usually `class='layer'`).
|
||||
*
|
||||
* @param {SVGGElement} elem - The SVG element to update
|
||||
* @returns {void}
|
||||
*/
|
||||
function addLayerClass (elem) {
|
||||
const classes = elem.getAttribute('class');
|
||||
if (isNullish(classes) || !classes.length) {
|
||||
elem.setAttribute('class', Layer.CLASS_NAME);
|
||||
} else if (!Layer.CLASS_REGEX.test(classes)) {
|
||||
elem.setAttribute('class', classes + ' ' + Layer.CLASS_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
export default Layer;
|
||||
223
src/common/math.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Mathematical utilities.
|
||||
* @module math
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:math.AngleCoord45
|
||||
* @property {Float} x - The angle-snapped x value
|
||||
* @property {Float} y - The angle-snapped y value
|
||||
* @property {Integer} a - The angle at which to snap
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:math.XYObject
|
||||
* @property {Float} x
|
||||
* @property {Float} y
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
import {getTransformList} from './svgtransformlist.js';
|
||||
import {isNullish} from './utilities.js';
|
||||
|
||||
// Constants
|
||||
const NEAR_ZERO = 1e-14;
|
||||
|
||||
// Throw away SVGSVGElement used for creating matrices/transforms.
|
||||
const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
|
||||
/**
|
||||
* A (hopefully) quicker function to transform a point by a matrix
|
||||
* (this function avoids any DOM calls and just does the math).
|
||||
* @function module:math.transformPoint
|
||||
* @param {Float} x - Float representing the x coordinate
|
||||
* @param {Float} y - Float representing the y coordinate
|
||||
* @param {SVGMatrix} m - Matrix object to transform the point with
|
||||
* @returns {module:math.XYObject} An x, y object representing the transformed point
|
||||
*/
|
||||
export const transformPoint = function (x, y, m) {
|
||||
return {x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f};
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to check if the matrix performs no actual transform
|
||||
* (i.e. exists for identity purposes).
|
||||
* @function module:math.isIdentity
|
||||
* @param {SVGMatrix} m - The matrix object to check
|
||||
* @returns {boolean} Indicates whether or not the matrix is 1,0,0,1,0,0
|
||||
*/
|
||||
export const isIdentity = function (m) {
|
||||
return (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function tries to return a `SVGMatrix` that is the multiplication `m1 * m2`.
|
||||
* We also round to zero when it's near zero.
|
||||
* @function module:math.matrixMultiply
|
||||
* @param {...SVGMatrix} args - Matrix objects to multiply
|
||||
* @returns {SVGMatrix} The matrix object resulting from the calculation
|
||||
*/
|
||||
export const matrixMultiply = function (...args) {
|
||||
const m = args.reduceRight((prev, m1) => {
|
||||
return m1.multiply(prev);
|
||||
});
|
||||
|
||||
if (Math.abs(m.a) < NEAR_ZERO) { m.a = 0; }
|
||||
if (Math.abs(m.b) < NEAR_ZERO) { m.b = 0; }
|
||||
if (Math.abs(m.c) < NEAR_ZERO) { m.c = 0; }
|
||||
if (Math.abs(m.d) < NEAR_ZERO) { m.d = 0; }
|
||||
if (Math.abs(m.e) < NEAR_ZERO) { m.e = 0; }
|
||||
if (Math.abs(m.f) < NEAR_ZERO) { m.f = 0; }
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
/**
|
||||
* See if the given transformlist includes a non-indentity matrix transform.
|
||||
* @function module:math.hasMatrixTransform
|
||||
* @param {SVGTransformList} [tlist] - The transformlist to check
|
||||
* @returns {boolean} Whether or not a matrix transform was found
|
||||
*/
|
||||
export const hasMatrixTransform = function (tlist) {
|
||||
if (!tlist) { return false; }
|
||||
let num = tlist.numberOfItems;
|
||||
while (num--) {
|
||||
const xform = tlist.getItem(num);
|
||||
if (xform.type === 1 && !isIdentity(xform.matrix)) { return true; }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:math.TransformedBox An object with the following values
|
||||
* @property {module:math.XYObject} tl - The top left coordinate
|
||||
* @property {module:math.XYObject} tr - The top right coordinate
|
||||
* @property {module:math.XYObject} bl - The bottom left coordinate
|
||||
* @property {module:math.XYObject} br - The bottom right coordinate
|
||||
* @property {PlainObject} aabox - Object with the following values:
|
||||
* @property {Float} aabox.x - Float with the axis-aligned x coordinate
|
||||
* @property {Float} aabox.y - Float with the axis-aligned y coordinate
|
||||
* @property {Float} aabox.width - Float with the axis-aligned width coordinate
|
||||
* @property {Float} aabox.height - Float with the axis-aligned height coordinate
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms a rectangle based on the given matrix.
|
||||
* @function module:math.transformBox
|
||||
* @param {Float} l - Float with the box's left coordinate
|
||||
* @param {Float} t - Float with the box's top coordinate
|
||||
* @param {Float} w - Float with the box width
|
||||
* @param {Float} h - Float with the box height
|
||||
* @param {SVGMatrix} m - Matrix object to transform the box by
|
||||
* @returns {module:math.TransformedBox}
|
||||
*/
|
||||
export const transformBox = function (l, t, w, h, m) {
|
||||
const tl = transformPoint(l, t, m),
|
||||
tr = transformPoint((l + w), t, m),
|
||||
bl = transformPoint(l, (t + h), m),
|
||||
br = transformPoint((l + w), (t + h), m),
|
||||
|
||||
minx = Math.min(tl.x, tr.x, bl.x, br.x),
|
||||
maxx = Math.max(tl.x, tr.x, bl.x, br.x),
|
||||
miny = Math.min(tl.y, tr.y, bl.y, br.y),
|
||||
maxy = Math.max(tl.y, tr.y, bl.y, br.y);
|
||||
|
||||
return {
|
||||
tl,
|
||||
tr,
|
||||
bl,
|
||||
br,
|
||||
aabox: {
|
||||
x: minx,
|
||||
y: miny,
|
||||
width: (maxx - minx),
|
||||
height: (maxy - miny)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This returns a single matrix Transform for a given Transform List
|
||||
* (this is the equivalent of `SVGTransformList.consolidate()` but unlike
|
||||
* that method, this one does not modify the actual `SVGTransformList`).
|
||||
* This function is very liberal with its `min`, `max` arguments.
|
||||
* @function module:math.transformListToTransform
|
||||
* @param {SVGTransformList} tlist - The transformlist object
|
||||
* @param {Integer} [min=0] - Optional integer indicating start transform position
|
||||
* @param {Integer} [max] - Optional integer indicating end transform position;
|
||||
* defaults to one less than the tlist's `numberOfItems`
|
||||
* @returns {SVGTransform} A single matrix transform object
|
||||
*/
|
||||
export const transformListToTransform = function (tlist, min, max) {
|
||||
if (isNullish(tlist)) {
|
||||
// Or should tlist = null have been prevented before this?
|
||||
return svg.createSVGTransformFromMatrix(svg.createSVGMatrix());
|
||||
}
|
||||
min = min || 0;
|
||||
max = max || (tlist.numberOfItems - 1);
|
||||
min = Number.parseInt(min);
|
||||
max = Number.parseInt(max);
|
||||
if (min > max) { const temp = max; max = min; min = temp; }
|
||||
let m = svg.createSVGMatrix();
|
||||
for (let i = min; i <= max; ++i) {
|
||||
// if our indices are out of range, just use a harmless identity matrix
|
||||
const mtom = (i >= 0 && i < tlist.numberOfItems
|
||||
? tlist.getItem(i).matrix
|
||||
: svg.createSVGMatrix());
|
||||
m = matrixMultiply(m, mtom);
|
||||
}
|
||||
return svg.createSVGTransformFromMatrix(m);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the matrix object for a given element.
|
||||
* @function module:math.getMatrix
|
||||
* @param {Element} elem - The DOM element to check
|
||||
* @returns {SVGMatrix} The matrix object associated with the element's transformlist
|
||||
*/
|
||||
export const getMatrix = function (elem) {
|
||||
const tlist = getTransformList(elem);
|
||||
return transformListToTransform(tlist).matrix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a 45 degree angle coordinate associated with the two given
|
||||
* coordinates.
|
||||
* @function module:math.snapToAngle
|
||||
* @param {Integer} x1 - First coordinate's x value
|
||||
* @param {Integer} y1 - First coordinate's y value
|
||||
* @param {Integer} x2 - Second coordinate's x value
|
||||
* @param {Integer} y2 - Second coordinate's y value
|
||||
* @returns {module:math.AngleCoord45}
|
||||
*/
|
||||
export const snapToAngle = function (x1, y1, x2, y2) {
|
||||
const snap = Math.PI / 4; // 45 degrees
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const snapangle = Math.round(angle / snap) * snap;
|
||||
|
||||
return {
|
||||
x: x1 + dist * Math.cos(snapangle),
|
||||
y: y1 + dist * Math.sin(snapangle),
|
||||
a: snapangle
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if two rectangles (BBoxes objects) intersect each other.
|
||||
* @function module:math.rectsIntersect
|
||||
* @param {SVGRect} r1 - The first BBox-like object
|
||||
* @param {SVGRect} r2 - The second BBox-like object
|
||||
* @returns {boolean} True if rectangles intersect
|
||||
*/
|
||||
export const rectsIntersect = function (r1, r2) {
|
||||
return r2.x < (r1.x + r1.width) &&
|
||||
(r2.x + r2.width) > r1.x &&
|
||||
r2.y < (r1.y + r1.height) &&
|
||||
(r2.y + r2.height) > r1.y;
|
||||
};
|
||||
33
src/common/namespaces.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Namespaces or tools therefor.
|
||||
* @module namespaces
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Common namepaces constants in alpha order.
|
||||
* @enum {string}
|
||||
* @type {PlainObject}
|
||||
* @memberof module:namespaces
|
||||
*/
|
||||
export const NS = {
|
||||
HTML: 'http://www.w3.org/1999/xhtml',
|
||||
MATH: 'http://www.w3.org/1998/Math/MathML',
|
||||
SE: 'http://svg-edit.googlecode.com',
|
||||
SVG: 'http://www.w3.org/2000/svg',
|
||||
XLINK: 'http://www.w3.org/1999/xlink',
|
||||
XML: 'http://www.w3.org/XML/1998/namespace',
|
||||
XMLNS: 'http://www.w3.org/2000/xmlns/' // see http://www.w3.org/TR/REC-xml-names/#xmlReserved
|
||||
};
|
||||
|
||||
/**
|
||||
* @function module:namespaces.getReverseNS
|
||||
* @returns {string} The NS with key values switched and lowercase
|
||||
*/
|
||||
export const getReverseNS = function () {
|
||||
const reverseNS = {};
|
||||
Object.entries(NS).forEach(([name, URI]) => {
|
||||
reverseNS[URI] = name.toLowerCase();
|
||||
});
|
||||
return reverseNS;
|
||||
};
|
||||
971
src/common/svgpathseg.js
Normal file
@@ -0,0 +1,971 @@
|
||||
/* globals SVGPathSeg, SVGPathSegMovetoRel, SVGPathSegMovetoAbs,
|
||||
SVGPathSegMovetoRel, SVGPathSegLinetoRel, SVGPathSegLinetoAbs,
|
||||
SVGPathSegLinetoHorizontalRel, SVGPathSegLinetoHorizontalAbs,
|
||||
SVGPathSegLinetoVerticalRel, SVGPathSegLinetoVerticalAbs,
|
||||
SVGPathSegClosePath, SVGPathSegCurvetoCubicRel,
|
||||
SVGPathSegCurvetoCubicAbs, SVGPathSegCurvetoCubicSmoothRel,
|
||||
SVGPathSegCurvetoCubicSmoothAbs, SVGPathSegCurvetoQuadraticRel,
|
||||
SVGPathSegCurvetoQuadraticAbs, SVGPathSegCurvetoQuadraticSmoothRel,
|
||||
SVGPathSegCurvetoQuadraticSmoothAbs, SVGPathSegArcRel, SVGPathSegArcAbs */
|
||||
/**
|
||||
* SVGPathSeg API polyfill
|
||||
* https://github.com/progers/pathseg
|
||||
*
|
||||
* This is a drop-in replacement for the `SVGPathSeg` and `SVGPathSegList` APIs
|
||||
* that were removed from SVG2 ({@link https://lists.w3.org/Archives/Public/www-svg/2015Jun/0044.html}),
|
||||
* including the latest spec changes which were implemented in Firefox 43 and
|
||||
* Chrome 46.
|
||||
*/
|
||||
/* eslint-disable no-shadow, class-methods-use-this, jsdoc/require-jsdoc */
|
||||
// Linting: We avoid `no-shadow` as ESLint thinks these are still available globals
|
||||
// Linting: We avoid `class-methods-use-this` as this is a polyfill that must
|
||||
// follow the conventions
|
||||
(() => {
|
||||
if (!('SVGPathSeg' in window)) {
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSeg
|
||||
class SVGPathSeg {
|
||||
constructor (type, typeAsLetter, owningPathSegList) {
|
||||
this.pathSegType = type;
|
||||
this.pathSegTypeAsLetter = typeAsLetter;
|
||||
this._owningPathSegList = owningPathSegList;
|
||||
}
|
||||
// Notify owning PathSegList on any changes so they can be synchronized back to the path element.
|
||||
_segmentChanged () {
|
||||
if (this._owningPathSegList) {
|
||||
this._owningPathSegList.segmentChanged(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
SVGPathSeg.prototype.classname = 'SVGPathSeg';
|
||||
|
||||
SVGPathSeg.PATHSEG_UNKNOWN = 0;
|
||||
SVGPathSeg.PATHSEG_CLOSEPATH = 1;
|
||||
SVGPathSeg.PATHSEG_MOVETO_ABS = 2;
|
||||
SVGPathSeg.PATHSEG_MOVETO_REL = 3;
|
||||
SVGPathSeg.PATHSEG_LINETO_ABS = 4;
|
||||
SVGPathSeg.PATHSEG_LINETO_REL = 5;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS = 6;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL = 7;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS = 8;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL = 9;
|
||||
SVGPathSeg.PATHSEG_ARC_ABS = 10;
|
||||
SVGPathSeg.PATHSEG_ARC_REL = 11;
|
||||
SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS = 12;
|
||||
SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL = 13;
|
||||
SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS = 14;
|
||||
SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL = 15;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS = 16;
|
||||
SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL = 17;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS = 18;
|
||||
SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL = 19;
|
||||
|
||||
class SVGPathSegClosePath extends SVGPathSeg {
|
||||
constructor (owningPathSegList) {
|
||||
super(SVGPathSeg.PATHSEG_CLOSEPATH, 'z', owningPathSegList);
|
||||
}
|
||||
toString () { return '[object SVGPathSegClosePath]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter; }
|
||||
clone () { return new SVGPathSegClosePath(undefined); }
|
||||
}
|
||||
|
||||
class SVGPathSegMovetoAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_MOVETO_ABS, 'M', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegMovetoAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegMovetoAbs(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegMovetoAbs.prototype, {
|
||||
x: {
|
||||
get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true
|
||||
},
|
||||
y: {
|
||||
get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true
|
||||
}
|
||||
});
|
||||
|
||||
class SVGPathSegMovetoRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_MOVETO_REL, 'm', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegMovetoRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegMovetoRel(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegMovetoRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegLinetoAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_ABS, 'L', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoAbs(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegLinetoAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegLinetoRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_REL, 'l', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoRel(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegLinetoRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoCubicAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS, 'C', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicAbs(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoCubicRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL, 'c', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicRel(undefined, this._x, this._y, this._x1, this._y1, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS, 'Q', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticAbs(undefined, this._x, this._y, this._x1, this._y1); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x1, y1) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL, 'q', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x1 = x1;
|
||||
this._y1 = y1;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x1 + ' ' + this._y1 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticRel(undefined, this._x, this._y, this._x1, this._y1); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x1: {get () { return this._x1; }, set (x1) { this._x1 = x1; this._segmentChanged(); }, enumerable: true},
|
||||
y1: {get () { return this._y1; }, set (y1) { this._y1 = y1; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegArcAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {
|
||||
super(SVGPathSeg.PATHSEG_ARC_ABS, 'A', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._r1 = r1;
|
||||
this._r2 = r2;
|
||||
this._angle = angle;
|
||||
this._largeArcFlag = largeArcFlag;
|
||||
this._sweepFlag = sweepFlag;
|
||||
}
|
||||
toString () { return '[object SVGPathSegArcAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegArcAbs(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegArcAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
r1: {get () { return this._r1; }, set (r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true},
|
||||
r2: {get () { return this._r2; }, set (r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true},
|
||||
angle: {get () { return this._angle; }, set (angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true},
|
||||
largeArcFlag: {get () { return this._largeArcFlag; }, set (largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true},
|
||||
sweepFlag: {get () { return this._sweepFlag; }, set (sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegArcRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, r1, r2, angle, largeArcFlag, sweepFlag) {
|
||||
super(SVGPathSeg.PATHSEG_ARC_REL, 'a', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._r1 = r1;
|
||||
this._r2 = r2;
|
||||
this._angle = angle;
|
||||
this._largeArcFlag = largeArcFlag;
|
||||
this._sweepFlag = sweepFlag;
|
||||
}
|
||||
toString () { return '[object SVGPathSegArcRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._r1 + ' ' + this._r2 + ' ' + this._angle + ' ' + (this._largeArcFlag ? '1' : '0') + ' ' + (this._sweepFlag ? '1' : '0') + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegArcRel(undefined, this._x, this._y, this._r1, this._r2, this._angle, this._largeArcFlag, this._sweepFlag); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegArcRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
r1: {get () { return this._r1; }, set (r1) { this._r1 = r1; this._segmentChanged(); }, enumerable: true},
|
||||
r2: {get () { return this._r2; }, set (r2) { this._r2 = r2; this._segmentChanged(); }, enumerable: true},
|
||||
angle: {get () { return this._angle; }, set (angle) { this._angle = angle; this._segmentChanged(); }, enumerable: true},
|
||||
largeArcFlag: {get () { return this._largeArcFlag; }, set (largeArcFlag) { this._largeArcFlag = largeArcFlag; this._segmentChanged(); }, enumerable: true},
|
||||
sweepFlag: {get () { return this._sweepFlag; }, set (sweepFlag) { this._sweepFlag = sweepFlag; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegLinetoHorizontalAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS, 'H', owningPathSegList);
|
||||
this._x = x;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoHorizontalAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x; }
|
||||
clone () { return new SVGPathSegLinetoHorizontalAbs(undefined, this._x); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoHorizontalAbs.prototype, 'x', {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegLinetoHorizontalRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL, 'h', owningPathSegList);
|
||||
this._x = x;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoHorizontalRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x; }
|
||||
clone () { return new SVGPathSegLinetoHorizontalRel(undefined, this._x); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoHorizontalRel.prototype, 'x', {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegLinetoVerticalAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS, 'V', owningPathSegList);
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoVerticalAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoVerticalAbs(undefined, this._y); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoVerticalAbs.prototype, 'y', {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegLinetoVerticalRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, y) {
|
||||
super(SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL, 'v', owningPathSegList);
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegLinetoVerticalRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegLinetoVerticalRel(undefined, this._y); }
|
||||
}
|
||||
Object.defineProperty(SVGPathSegLinetoVerticalRel.prototype, 'y', {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true});
|
||||
|
||||
class SVGPathSegCurvetoCubicSmoothAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS, 'S', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicSmoothAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicSmoothAbs(undefined, this._x, this._y, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicSmoothAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoCubicSmoothRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y, x2, y2) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL, 's', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._x2 = x2;
|
||||
this._y2 = y2;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoCubicSmoothRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x2 + ' ' + this._y2 + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoCubicSmoothRel(undefined, this._x, this._y, this._x2, this._y2); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoCubicSmoothRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true},
|
||||
x2: {get () { return this._x2; }, set (x2) { this._x2 = x2; this._segmentChanged(); }, enumerable: true},
|
||||
y2: {get () { return this._y2; }, set (y2) { this._y2 = y2; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticSmoothAbs extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS, 'T', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticSmoothAbs]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticSmoothAbs(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticSmoothAbs.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
class SVGPathSegCurvetoQuadraticSmoothRel extends SVGPathSeg {
|
||||
constructor (owningPathSegList, x, y) {
|
||||
super(SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL, 't', owningPathSegList);
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
toString () { return '[object SVGPathSegCurvetoQuadraticSmoothRel]'; }
|
||||
_asPathString () { return this.pathSegTypeAsLetter + ' ' + this._x + ' ' + this._y; }
|
||||
clone () { return new SVGPathSegCurvetoQuadraticSmoothRel(undefined, this._x, this._y); }
|
||||
}
|
||||
Object.defineProperties(SVGPathSegCurvetoQuadraticSmoothRel.prototype, {
|
||||
x: {get () { return this._x; }, set (x) { this._x = x; this._segmentChanged(); }, enumerable: true},
|
||||
y: {get () { return this._y; }, set (y) { this._y = y; this._segmentChanged(); }, enumerable: true}
|
||||
});
|
||||
|
||||
// Add createSVGPathSeg* functions to SVGPathElement.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathElement.
|
||||
SVGPathElement.prototype.createSVGPathSegClosePath = function () { return new SVGPathSegClosePath(undefined); };
|
||||
SVGPathElement.prototype.createSVGPathSegMovetoAbs = function (x, y) { return new SVGPathSegMovetoAbs(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegMovetoRel = function (x, y) { return new SVGPathSegMovetoRel(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoAbs = function (x, y) { return new SVGPathSegLinetoAbs(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoRel = function (x, y) { return new SVGPathSegLinetoRel(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicAbs = function (x, y, x1, y1, x2, y2) { return new SVGPathSegCurvetoCubicAbs(undefined, x, y, x1, y1, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicRel = function (x, y, x1, y1, x2, y2) { return new SVGPathSegCurvetoCubicRel(undefined, x, y, x1, y1, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticAbs = function (x, y, x1, y1) { return new SVGPathSegCurvetoQuadraticAbs(undefined, x, y, x1, y1); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticRel = function (x, y, x1, y1) { return new SVGPathSegCurvetoQuadraticRel(undefined, x, y, x1, y1); };
|
||||
SVGPathElement.prototype.createSVGPathSegArcAbs = function (x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new SVGPathSegArcAbs(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); };
|
||||
SVGPathElement.prototype.createSVGPathSegArcRel = function (x, y, r1, r2, angle, largeArcFlag, sweepFlag) { return new SVGPathSegArcRel(undefined, x, y, r1, r2, angle, largeArcFlag, sweepFlag); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoHorizontalAbs = function (x) { return new SVGPathSegLinetoHorizontalAbs(undefined, x); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoHorizontalRel = function (x) { return new SVGPathSegLinetoHorizontalRel(undefined, x); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoVerticalAbs = function (y) { return new SVGPathSegLinetoVerticalAbs(undefined, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegLinetoVerticalRel = function (y) { return new SVGPathSegLinetoVerticalRel(undefined, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothAbs = function (x, y, x2, y2) { return new SVGPathSegCurvetoCubicSmoothAbs(undefined, x, y, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoCubicSmoothRel = function (x, y, x2, y2) { return new SVGPathSegCurvetoCubicSmoothRel(undefined, x, y, x2, y2); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothAbs = function (x, y) { return new SVGPathSegCurvetoQuadraticSmoothAbs(undefined, x, y); };
|
||||
SVGPathElement.prototype.createSVGPathSegCurvetoQuadraticSmoothRel = function (x, y) { return new SVGPathSegCurvetoQuadraticSmoothRel(undefined, x, y); };
|
||||
|
||||
if (!('getPathSegAtLength' in SVGPathElement.prototype)) {
|
||||
// Add getPathSegAtLength to SVGPathElement.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-__svg__SVGPathElement__getPathSegAtLength
|
||||
// This polyfill requires SVGPathElement.getTotalLength to implement the distance-along-a-path algorithm.
|
||||
SVGPathElement.prototype.getPathSegAtLength = function (distance) {
|
||||
if (distance === undefined || !isFinite(distance)) {
|
||||
throw new Error('Invalid arguments.');
|
||||
}
|
||||
|
||||
const measurementElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
measurementElement.setAttribute('d', this.getAttribute('d'));
|
||||
let lastPathSegment = measurementElement.pathSegList.numberOfItems - 1;
|
||||
|
||||
// If the path is empty, return 0.
|
||||
if (lastPathSegment <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
do {
|
||||
measurementElement.pathSegList.removeItem(lastPathSegment);
|
||||
if (distance > measurementElement.getTotalLength()) {
|
||||
break;
|
||||
}
|
||||
lastPathSegment--;
|
||||
} while (lastPathSegment > 0);
|
||||
return lastPathSegment;
|
||||
};
|
||||
}
|
||||
|
||||
window.SVGPathSeg = SVGPathSeg;
|
||||
window.SVGPathSegClosePath = SVGPathSegClosePath;
|
||||
window.SVGPathSegMovetoAbs = SVGPathSegMovetoAbs;
|
||||
window.SVGPathSegMovetoRel = SVGPathSegMovetoRel;
|
||||
window.SVGPathSegLinetoAbs = SVGPathSegLinetoAbs;
|
||||
window.SVGPathSegLinetoRel = SVGPathSegLinetoRel;
|
||||
window.SVGPathSegCurvetoCubicAbs = SVGPathSegCurvetoCubicAbs;
|
||||
window.SVGPathSegCurvetoCubicRel = SVGPathSegCurvetoCubicRel;
|
||||
window.SVGPathSegCurvetoQuadraticAbs = SVGPathSegCurvetoQuadraticAbs;
|
||||
window.SVGPathSegCurvetoQuadraticRel = SVGPathSegCurvetoQuadraticRel;
|
||||
window.SVGPathSegArcAbs = SVGPathSegArcAbs;
|
||||
window.SVGPathSegArcRel = SVGPathSegArcRel;
|
||||
window.SVGPathSegLinetoHorizontalAbs = SVGPathSegLinetoHorizontalAbs;
|
||||
window.SVGPathSegLinetoHorizontalRel = SVGPathSegLinetoHorizontalRel;
|
||||
window.SVGPathSegLinetoVerticalAbs = SVGPathSegLinetoVerticalAbs;
|
||||
window.SVGPathSegLinetoVerticalRel = SVGPathSegLinetoVerticalRel;
|
||||
window.SVGPathSegCurvetoCubicSmoothAbs = SVGPathSegCurvetoCubicSmoothAbs;
|
||||
window.SVGPathSegCurvetoCubicSmoothRel = SVGPathSegCurvetoCubicSmoothRel;
|
||||
window.SVGPathSegCurvetoQuadraticSmoothAbs = SVGPathSegCurvetoQuadraticSmoothAbs;
|
||||
window.SVGPathSegCurvetoQuadraticSmoothRel = SVGPathSegCurvetoQuadraticSmoothRel;
|
||||
}
|
||||
|
||||
// Checking for SVGPathSegList in window checks for the case of an implementation without the
|
||||
// SVGPathSegList API.
|
||||
// The second check for appendItem is specific to Firefox 59+ which removed only parts of the
|
||||
// SVGPathSegList API (e.g., appendItem). In this case we need to re-implement the entire API
|
||||
// so the polyfill data (i.e., _list) is used throughout.
|
||||
if (!('SVGPathSegList' in window) || !('appendItem' in window.SVGPathSegList.prototype)) {
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGPathSegList
|
||||
class SVGPathSegList {
|
||||
constructor (pathElement) {
|
||||
this._pathElement = pathElement;
|
||||
this._list = this._parsePath(this._pathElement.getAttribute('d'));
|
||||
|
||||
// Use a MutationObserver to catch changes to the path's "d" attribute.
|
||||
this._mutationObserverConfig = {attributes: true, attributeFilter: ['d']};
|
||||
this._pathElementMutationObserver = new MutationObserver(this._updateListFromPathMutations.bind(this));
|
||||
this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);
|
||||
}
|
||||
// Process any pending mutations to the path element and update the list as needed.
|
||||
// This should be the first call of all public functions and is needed because
|
||||
// MutationObservers are not synchronous so we can have pending asynchronous mutations.
|
||||
_checkPathSynchronizedToList () {
|
||||
this._updateListFromPathMutations(this._pathElementMutationObserver.takeRecords());
|
||||
}
|
||||
|
||||
_updateListFromPathMutations (mutationRecords) {
|
||||
if (!this._pathElement) {
|
||||
return;
|
||||
}
|
||||
let hasPathMutations = false;
|
||||
mutationRecords.forEach((record) => {
|
||||
if (record.attributeName === 'd') {
|
||||
hasPathMutations = true;
|
||||
}
|
||||
});
|
||||
if (hasPathMutations) {
|
||||
this._list = this._parsePath(this._pathElement.getAttribute('d'));
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the list and update the path's 'd' attribute.
|
||||
_writeListToPath () {
|
||||
this._pathElementMutationObserver.disconnect();
|
||||
this._pathElement.setAttribute('d', SVGPathSegList._pathSegArrayAsString(this._list));
|
||||
this._pathElementMutationObserver.observe(this._pathElement, this._mutationObserverConfig);
|
||||
}
|
||||
|
||||
// When a path segment changes the list needs to be synchronized back to the path element.
|
||||
segmentChanged (pathSeg) {
|
||||
this._writeListToPath();
|
||||
}
|
||||
|
||||
clear () {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._list.forEach((pathSeg) => {
|
||||
pathSeg._owningPathSegList = null;
|
||||
});
|
||||
this._list = [];
|
||||
this._writeListToPath();
|
||||
}
|
||||
|
||||
initialize (newItem) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._list = [newItem];
|
||||
newItem._owningPathSegList = this;
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
_checkValidIndex (index) {
|
||||
if (isNaN(index) || index < 0 || index >= this.numberOfItems) {
|
||||
throw new Error('INDEX_SIZE_ERR');
|
||||
}
|
||||
}
|
||||
|
||||
getItem (index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._checkValidIndex(index);
|
||||
return this._list[index];
|
||||
}
|
||||
|
||||
insertItemBefore (newItem, index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
// Spec: If the index is greater than or equal to numberOfItems, then the new item is appended to the end of the list.
|
||||
if (index > this.numberOfItems) {
|
||||
index = this.numberOfItems;
|
||||
}
|
||||
if (newItem._owningPathSegList) {
|
||||
// SVG2 spec says to make a copy.
|
||||
newItem = newItem.clone();
|
||||
}
|
||||
this._list.splice(index, 0, newItem);
|
||||
newItem._owningPathSegList = this;
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
replaceItem (newItem, index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
if (newItem._owningPathSegList) {
|
||||
// SVG2 spec says to make a copy.
|
||||
newItem = newItem.clone();
|
||||
}
|
||||
this._checkValidIndex(index);
|
||||
this._list[index] = newItem;
|
||||
newItem._owningPathSegList = this;
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
removeItem (index) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
this._checkValidIndex(index);
|
||||
const item = this._list[index];
|
||||
this._list.splice(index, 1);
|
||||
this._writeListToPath();
|
||||
return item;
|
||||
}
|
||||
|
||||
appendItem (newItem) {
|
||||
this._checkPathSynchronizedToList();
|
||||
|
||||
if (newItem._owningPathSegList) {
|
||||
// SVG2 spec says to make a copy.
|
||||
newItem = newItem.clone();
|
||||
}
|
||||
this._list.push(newItem);
|
||||
newItem._owningPathSegList = this;
|
||||
// TODO: Optimize this to just append to the existing attribute.
|
||||
this._writeListToPath();
|
||||
return newItem;
|
||||
}
|
||||
|
||||
// This closely follows SVGPathParser::parsePath from Source/core/svg/SVGPathParser.cpp.
|
||||
_parsePath (string) {
|
||||
if (!string || !string.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const owningPathSegList = this; // eslint-disable-line consistent-this
|
||||
|
||||
class Builder {
|
||||
constructor () {
|
||||
this.pathSegList = [];
|
||||
}
|
||||
appendSegment (pathSeg) {
|
||||
this.pathSegList.push(pathSeg);
|
||||
}
|
||||
}
|
||||
|
||||
class Source {
|
||||
constructor (string) {
|
||||
this._string = string;
|
||||
this._currentIndex = 0;
|
||||
this._endIndex = this._string.length;
|
||||
this._previousCommand = SVGPathSeg.PATHSEG_UNKNOWN;
|
||||
|
||||
this._skipOptionalSpaces();
|
||||
}
|
||||
_isCurrentSpace () {
|
||||
const character = this._string[this._currentIndex];
|
||||
return character <= ' ' && (character === ' ' || character === '\n' || character === '\t' || character === '\r' || character === '\f');
|
||||
}
|
||||
|
||||
_skipOptionalSpaces () {
|
||||
while (this._currentIndex < this._endIndex && this._isCurrentSpace()) {
|
||||
this._currentIndex++;
|
||||
}
|
||||
return this._currentIndex < this._endIndex;
|
||||
}
|
||||
|
||||
_skipOptionalSpacesOrDelimiter () {
|
||||
if (this._currentIndex < this._endIndex && !this._isCurrentSpace() && this._string.charAt(this._currentIndex) !== ',') {
|
||||
return false;
|
||||
}
|
||||
if (this._skipOptionalSpaces()) {
|
||||
if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === ',') {
|
||||
this._currentIndex++;
|
||||
this._skipOptionalSpaces();
|
||||
}
|
||||
}
|
||||
return this._currentIndex < this._endIndex;
|
||||
}
|
||||
|
||||
hasMoreData () {
|
||||
return this._currentIndex < this._endIndex;
|
||||
}
|
||||
|
||||
peekSegmentType () {
|
||||
const lookahead = this._string[this._currentIndex];
|
||||
return this._pathSegTypeFromChar(lookahead);
|
||||
}
|
||||
|
||||
_pathSegTypeFromChar (lookahead) {
|
||||
switch (lookahead) {
|
||||
case 'Z':
|
||||
case 'z':
|
||||
return SVGPathSeg.PATHSEG_CLOSEPATH;
|
||||
case 'M':
|
||||
return SVGPathSeg.PATHSEG_MOVETO_ABS;
|
||||
case 'm':
|
||||
return SVGPathSeg.PATHSEG_MOVETO_REL;
|
||||
case 'L':
|
||||
return SVGPathSeg.PATHSEG_LINETO_ABS;
|
||||
case 'l':
|
||||
return SVGPathSeg.PATHSEG_LINETO_REL;
|
||||
case 'C':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS;
|
||||
case 'c':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL;
|
||||
case 'Q':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS;
|
||||
case 'q':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL;
|
||||
case 'A':
|
||||
return SVGPathSeg.PATHSEG_ARC_ABS;
|
||||
case 'a':
|
||||
return SVGPathSeg.PATHSEG_ARC_REL;
|
||||
case 'H':
|
||||
return SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS;
|
||||
case 'h':
|
||||
return SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL;
|
||||
case 'V':
|
||||
return SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS;
|
||||
case 'v':
|
||||
return SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL;
|
||||
case 'S':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS;
|
||||
case 's':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL;
|
||||
case 'T':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS;
|
||||
case 't':
|
||||
return SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL;
|
||||
default:
|
||||
return SVGPathSeg.PATHSEG_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
_nextCommandHelper (lookahead, previousCommand) {
|
||||
// Check for remaining coordinates in the current command.
|
||||
if ((lookahead === '+' || lookahead === '-' || lookahead === '.' || (lookahead >= '0' && lookahead <= '9')) && previousCommand !== SVGPathSeg.PATHSEG_CLOSEPATH) {
|
||||
if (previousCommand === SVGPathSeg.PATHSEG_MOVETO_ABS) {
|
||||
return SVGPathSeg.PATHSEG_LINETO_ABS;
|
||||
}
|
||||
if (previousCommand === SVGPathSeg.PATHSEG_MOVETO_REL) {
|
||||
return SVGPathSeg.PATHSEG_LINETO_REL;
|
||||
}
|
||||
return previousCommand;
|
||||
}
|
||||
return SVGPathSeg.PATHSEG_UNKNOWN;
|
||||
}
|
||||
|
||||
initialCommandIsMoveTo () {
|
||||
// If the path is empty it is still valid, so return true.
|
||||
if (!this.hasMoreData()) {
|
||||
return true;
|
||||
}
|
||||
const command = this.peekSegmentType();
|
||||
// Path must start with moveTo.
|
||||
return command === SVGPathSeg.PATHSEG_MOVETO_ABS || command === SVGPathSeg.PATHSEG_MOVETO_REL;
|
||||
}
|
||||
|
||||
// Parse a number from an SVG path. This very closely follows genericParseNumber(...) from Source/core/svg/SVGParserUtilities.cpp.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-PathDataBNF
|
||||
_parseNumber () {
|
||||
let exponent = 0;
|
||||
let integer = 0;
|
||||
let frac = 1;
|
||||
let decimal = 0;
|
||||
let sign = 1;
|
||||
let expsign = 1;
|
||||
|
||||
const startIndex = this._currentIndex;
|
||||
|
||||
this._skipOptionalSpaces();
|
||||
|
||||
// Read the sign.
|
||||
if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '+') {
|
||||
this._currentIndex++;
|
||||
} else if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '-') {
|
||||
this._currentIndex++;
|
||||
sign = -1;
|
||||
}
|
||||
|
||||
if (this._currentIndex === this._endIndex || ((this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') && this._string.charAt(this._currentIndex) !== '.')) {
|
||||
// The first character of a number must be one of [0-9+-.].
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Read the integer part, build right-to-left.
|
||||
const startIntPartIndex = this._currentIndex;
|
||||
while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
|
||||
this._currentIndex++; // Advance to first non-digit.
|
||||
}
|
||||
|
||||
if (this._currentIndex !== startIntPartIndex) {
|
||||
let scanIntPartIndex = this._currentIndex - 1;
|
||||
let multiplier = 1;
|
||||
while (scanIntPartIndex >= startIntPartIndex) {
|
||||
integer += multiplier * (this._string.charAt(scanIntPartIndex--) - '0');
|
||||
multiplier *= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the decimals.
|
||||
if (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) === '.') {
|
||||
this._currentIndex++;
|
||||
|
||||
// There must be a least one digit following the .
|
||||
if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') {
|
||||
return undefined;
|
||||
}
|
||||
while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
|
||||
frac *= 10;
|
||||
decimal += (this._string.charAt(this._currentIndex) - '0') / frac;
|
||||
this._currentIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the exponent part.
|
||||
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')) {
|
||||
this._currentIndex++;
|
||||
|
||||
// Read the sign of the exponent.
|
||||
if (this._string.charAt(this._currentIndex) === '+') {
|
||||
this._currentIndex++;
|
||||
} else if (this._string.charAt(this._currentIndex) === '-') {
|
||||
this._currentIndex++;
|
||||
expsign = -1;
|
||||
}
|
||||
|
||||
// There must be an exponent.
|
||||
if (this._currentIndex >= this._endIndex || this._string.charAt(this._currentIndex) < '0' || this._string.charAt(this._currentIndex) > '9') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
while (this._currentIndex < this._endIndex && this._string.charAt(this._currentIndex) >= '0' && this._string.charAt(this._currentIndex) <= '9') {
|
||||
exponent *= 10;
|
||||
exponent += (this._string.charAt(this._currentIndex) - '0');
|
||||
this._currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
let number = integer + decimal;
|
||||
number *= sign;
|
||||
|
||||
if (exponent) {
|
||||
number *= 10 ** (expsign * exponent);
|
||||
}
|
||||
|
||||
if (startIndex === this._currentIndex) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this._skipOptionalSpacesOrDelimiter();
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
_parseArcFlag () {
|
||||
if (this._currentIndex >= this._endIndex) {
|
||||
return undefined;
|
||||
}
|
||||
let flag = false;
|
||||
const flagChar = this._string.charAt(this._currentIndex++);
|
||||
if (flagChar === '0') {
|
||||
flag = false;
|
||||
} else if (flagChar === '1') {
|
||||
flag = true;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this._skipOptionalSpacesOrDelimiter();
|
||||
return flag;
|
||||
}
|
||||
|
||||
parseSegment () {
|
||||
const lookahead = this._string[this._currentIndex];
|
||||
let command = this._pathSegTypeFromChar(lookahead);
|
||||
if (command === SVGPathSeg.PATHSEG_UNKNOWN) {
|
||||
// Possibly an implicit command. Not allowed if this is the first command.
|
||||
if (this._previousCommand === SVGPathSeg.PATHSEG_UNKNOWN) {
|
||||
return null;
|
||||
}
|
||||
command = this._nextCommandHelper(lookahead, this._previousCommand);
|
||||
if (command === SVGPathSeg.PATHSEG_UNKNOWN) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
this._currentIndex++;
|
||||
}
|
||||
|
||||
this._previousCommand = command;
|
||||
|
||||
switch (command) {
|
||||
case SVGPathSeg.PATHSEG_MOVETO_REL:
|
||||
return new SVGPathSegMovetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_MOVETO_ABS:
|
||||
return new SVGPathSegMovetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_REL:
|
||||
return new SVGPathSegLinetoRel(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_ABS:
|
||||
return new SVGPathSegLinetoAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL:
|
||||
return new SVGPathSegLinetoHorizontalRel(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS:
|
||||
return new SVGPathSegLinetoHorizontalAbs(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL:
|
||||
return new SVGPathSegLinetoVerticalRel(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS:
|
||||
return new SVGPathSegLinetoVerticalAbs(owningPathSegList, this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_CLOSEPATH:
|
||||
this._skipOptionalSpaces();
|
||||
return new SVGPathSegClosePath(owningPathSegList);
|
||||
case SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL: {
|
||||
const points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicSmoothRel(owningPathSegList, points.x, points.y, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS: {
|
||||
const points = {x2: this._parseNumber(), y2: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoCubicSmoothAbs(owningPathSegList, points.x, points.y, points.x2, points.y2);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoQuadraticRel(owningPathSegList, points.x, points.y, points.x1, points.y1);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegCurvetoQuadraticAbs(owningPathSegList, points.x, points.y, points.x1, points.y1);
|
||||
} case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL:
|
||||
return new SVGPathSegCurvetoQuadraticSmoothRel(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS:
|
||||
return new SVGPathSegCurvetoQuadraticSmoothAbs(owningPathSegList, this._parseNumber(), this._parseNumber());
|
||||
case SVGPathSeg.PATHSEG_ARC_REL: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegArcRel(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);
|
||||
} case SVGPathSeg.PATHSEG_ARC_ABS: {
|
||||
const points = {x1: this._parseNumber(), y1: this._parseNumber(), arcAngle: this._parseNumber(), arcLarge: this._parseArcFlag(), arcSweep: this._parseArcFlag(), x: this._parseNumber(), y: this._parseNumber()};
|
||||
return new SVGPathSegArcAbs(owningPathSegList, points.x, points.y, points.x1, points.y1, points.arcAngle, points.arcLarge, points.arcSweep);
|
||||
} default:
|
||||
throw new Error('Unknown path seg type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const builder = new Builder();
|
||||
const source = new Source(string);
|
||||
|
||||
if (!source.initialCommandIsMoveTo()) {
|
||||
return [];
|
||||
}
|
||||
while (source.hasMoreData()) {
|
||||
const pathSeg = source.parseSegment();
|
||||
if (!pathSeg) {
|
||||
return [];
|
||||
}
|
||||
builder.appendSegment(pathSeg);
|
||||
}
|
||||
|
||||
return builder.pathSegList;
|
||||
}
|
||||
|
||||
// STATIC
|
||||
static _pathSegArrayAsString (pathSegArray) {
|
||||
let string = '';
|
||||
let first = true;
|
||||
pathSegArray.forEach((pathSeg) => {
|
||||
if (first) {
|
||||
first = false;
|
||||
string += pathSeg._asPathString();
|
||||
} else {
|
||||
string += ' ' + pathSeg._asPathString();
|
||||
}
|
||||
});
|
||||
return string;
|
||||
}
|
||||
}
|
||||
|
||||
SVGPathSegList.prototype.classname = 'SVGPathSegList';
|
||||
|
||||
Object.defineProperty(SVGPathSegList.prototype, 'numberOfItems', {
|
||||
get () {
|
||||
this._checkPathSynchronizedToList();
|
||||
return this._list.length;
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
// Add the pathSegList accessors to SVGPathElement.
|
||||
// Spec: https://www.w3.org/TR/SVG11/single-page.html#paths-InterfaceSVGAnimatedPathData
|
||||
Object.defineProperties(SVGPathElement.prototype, {
|
||||
pathSegList: {
|
||||
get () {
|
||||
if (!this._pathSegList) {
|
||||
this._pathSegList = new SVGPathSegList(this);
|
||||
}
|
||||
return this._pathSegList;
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
// TODO: The following are not implemented and simply return SVGPathElement.pathSegList.
|
||||
normalizedPathSegList: {get () { return this.pathSegList; }, enumerable: true},
|
||||
animatedPathSegList: {get () { return this.pathSegList; }, enumerable: true},
|
||||
animatedNormalizedPathSegList: {get () { return this.pathSegList; }, enumerable: true}
|
||||
});
|
||||
window.SVGPathSegList = SVGPathSegList;
|
||||
}
|
||||
})();
|
||||
397
src/common/svgtransformlist.js
Normal file
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* Partial polyfill of `SVGTransformList`
|
||||
* @module SVGTransformList
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
import {supportsNativeTransformLists} from './browser.js';
|
||||
|
||||
const svgroot = document.createElementNS(NS.SVG, 'svg');
|
||||
|
||||
/**
|
||||
* Helper function to convert `SVGTransform` to a string.
|
||||
* @param {SVGTransform} xform
|
||||
* @returns {string}
|
||||
*/
|
||||
function transformToString (xform) {
|
||||
const m = xform.matrix;
|
||||
let text = '';
|
||||
switch (xform.type) {
|
||||
case 1: // MATRIX
|
||||
text = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')';
|
||||
break;
|
||||
case 2: // TRANSLATE
|
||||
text = 'translate(' + m.e + ',' + m.f + ')';
|
||||
break;
|
||||
case 3: // SCALE
|
||||
if (m.a === m.d) {
|
||||
text = 'scale(' + m.a + ')';
|
||||
} else {
|
||||
text = 'scale(' + m.a + ',' + m.d + ')';
|
||||
}
|
||||
break;
|
||||
case 4: { // ROTATE
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
// this prevents divide by zero
|
||||
if (xform.angle !== 0) {
|
||||
const K = 1 - m.a;
|
||||
cy = (K * m.f + m.b * m.e) / (K * K + m.b * m.b);
|
||||
cx = (m.e - m.b * cy) / K;
|
||||
}
|
||||
text = 'rotate(' + xform.angle + ' ' + cx + ',' + cy + ')';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of SVGTransformList objects.
|
||||
*/
|
||||
let listMap_ = {};
|
||||
|
||||
/**
|
||||
* @interface module:SVGTransformList.SVGEditTransformList
|
||||
* @property {Integer} numberOfItems unsigned long
|
||||
*/
|
||||
/**
|
||||
* @function module:SVGTransformList.SVGEditTransformList#clear
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @function module:SVGTransformList.SVGEditTransformList#initialize
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#getItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#insertItemBefore
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#replaceItem
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
|
||||
* @function module:SVGTransformList.SVGEditTransformList#removeItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* @function module:SVGTransformList.SVGEditTransformList#appendItem
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* NOT IMPLEMENTED.
|
||||
* @ignore
|
||||
* @function module:SVGTransformList.SVGEditTransformList#createSVGTransformFromMatrix
|
||||
* @param {SVGMatrix} matrix
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
/**
|
||||
* NOT IMPLEMENTED.
|
||||
* @ignore
|
||||
* @function module:SVGTransformList.SVGEditTransformList#consolidate
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
|
||||
/**
|
||||
* SVGTransformList implementation for Webkit.
|
||||
* These methods do not currently raise any exceptions.
|
||||
* These methods also do not check that transforms are being inserted. This is basically
|
||||
* implementing as much of SVGTransformList that we need to get the job done.
|
||||
* @implements {module:SVGTransformList.SVGEditTransformList}
|
||||
*/
|
||||
export class SVGTransformList { // eslint-disable-line no-shadow
|
||||
/**
|
||||
* @param {Element} elem
|
||||
* @returns {SVGTransformList}
|
||||
*/
|
||||
constructor (elem) {
|
||||
this._elem = elem || null;
|
||||
this._xforms = [];
|
||||
// TODO: how do we capture the undo-ability in the changed transform list?
|
||||
this._update = function () {
|
||||
let tstr = '';
|
||||
// /* const concatMatrix = */ svgroot.createSVGMatrix();
|
||||
for (let i = 0; i < this.numberOfItems; ++i) {
|
||||
const xform = this._list.getItem(i);
|
||||
tstr += transformToString(xform) + ' ';
|
||||
}
|
||||
this._elem.setAttribute('transform', tstr);
|
||||
};
|
||||
this._list = this;
|
||||
this._init = function () {
|
||||
// Transform attribute parser
|
||||
let str = this._elem.getAttribute('transform');
|
||||
if (!str) { return; }
|
||||
|
||||
// TODO: Add skew support in future
|
||||
const re = /\s*((scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/;
|
||||
// const re = /\s*(?<xform>(?:scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/;
|
||||
let m = true;
|
||||
while (m) {
|
||||
m = str.match(re);
|
||||
str = str.replace(re, '');
|
||||
if (m && m[1]) {
|
||||
const x = m[1];
|
||||
const bits = x.split(/\s*\(/);
|
||||
const name = bits[0];
|
||||
const valBits = bits[1].match(/\s*(.*?)\s*\)/);
|
||||
valBits[1] = valBits[1].replace(/(\d)-/g, '$1 -');
|
||||
const valArr = valBits[1].split(/[, ]+/);
|
||||
const letters = 'abcdef'.split('');
|
||||
/*
|
||||
if (m && m.groups.xform) {
|
||||
const x = m.groups.xform;
|
||||
const [name, bits] = x.split(/\s*\(/);
|
||||
const valBits = bits.match(/\s*(?<nonWhitespace>.*?)\s*\)/);
|
||||
valBits.groups.nonWhitespace = valBits.groups.nonWhitespace.replace(
|
||||
/(?<digit>\d)-/g, '$<digit> -'
|
||||
);
|
||||
const valArr = valBits.groups.nonWhitespace.split(/[, ]+/);
|
||||
const letters = [...'abcdef'];
|
||||
*/
|
||||
const mtx = svgroot.createSVGMatrix();
|
||||
Object.values(valArr).forEach(function (item, i) {
|
||||
valArr[i] = Number.parseFloat(item);
|
||||
if (name === 'matrix') {
|
||||
mtx[letters[i]] = valArr[i];
|
||||
}
|
||||
});
|
||||
const xform = svgroot.createSVGTransform();
|
||||
const fname = 'set' + name.charAt(0).toUpperCase() + name.slice(1);
|
||||
const values = name === 'matrix' ? [mtx] : valArr;
|
||||
|
||||
if (name === 'scale' && values.length === 1) {
|
||||
values.push(values[0]);
|
||||
} else if (name === 'translate' && values.length === 1) {
|
||||
values.push(0);
|
||||
} else if (name === 'rotate' && values.length === 1) {
|
||||
values.push(0, 0);
|
||||
}
|
||||
xform[fname](...values);
|
||||
this._list.appendItem(xform);
|
||||
}
|
||||
}
|
||||
};
|
||||
this._removeFromOtherLists = function (item) {
|
||||
if (item) {
|
||||
// Check if this transform is already in a transformlist, and
|
||||
// remove it if so.
|
||||
Object.values(listMap_).some((tl) => {
|
||||
for (let i = 0, len = tl._xforms.length; i < len; ++i) {
|
||||
if (tl._xforms[i] === item) {
|
||||
tl.removeItem(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.numberOfItems = 0;
|
||||
}
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clear () {
|
||||
this.numberOfItems = 0;
|
||||
this._xforms = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {void}
|
||||
*/
|
||||
initialize (newItem) {
|
||||
this.numberOfItems = 1;
|
||||
this._removeFromOtherLists(newItem);
|
||||
this._xforms = [newItem];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Integer} index unsigned long
|
||||
* @throws {Error}
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
getItem (index) {
|
||||
if (index < this.numberOfItems && index >= 0) {
|
||||
return this._xforms[index];
|
||||
}
|
||||
const err = new Error('DOMException with code=INDEX_SIZE_ERR');
|
||||
err.code = 1;
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
insertItemBefore (newItem, index) {
|
||||
let retValue = null;
|
||||
if (index >= 0) {
|
||||
if (index < this.numberOfItems) {
|
||||
this._removeFromOtherLists(newItem);
|
||||
const newxforms = new Array(this.numberOfItems + 1);
|
||||
// TODO: use array copying and slicing
|
||||
let i;
|
||||
for (i = 0; i < index; ++i) {
|
||||
newxforms[i] = this._xforms[i];
|
||||
}
|
||||
newxforms[i] = newItem;
|
||||
for (let j = i + 1; i < this.numberOfItems; ++j, ++i) {
|
||||
newxforms[j] = this._xforms[i];
|
||||
}
|
||||
this.numberOfItems++;
|
||||
this._xforms = newxforms;
|
||||
retValue = newItem;
|
||||
this._list._update();
|
||||
} else {
|
||||
retValue = this._list.appendItem(newItem);
|
||||
}
|
||||
}
|
||||
return retValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @param {Integer} index unsigned long
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
replaceItem (newItem, index) {
|
||||
let retValue = null;
|
||||
if (index < this.numberOfItems && index >= 0) {
|
||||
this._removeFromOtherLists(newItem);
|
||||
this._xforms[index] = newItem;
|
||||
retValue = newItem;
|
||||
this._list._update();
|
||||
}
|
||||
return retValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Integer} index unsigned long
|
||||
* @throws {Error}
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
removeItem (index) {
|
||||
if (index < this.numberOfItems && index >= 0) {
|
||||
const retValue = this._xforms[index];
|
||||
const newxforms = new Array(this.numberOfItems - 1);
|
||||
let i;
|
||||
for (i = 0; i < index; ++i) {
|
||||
newxforms[i] = this._xforms[i];
|
||||
}
|
||||
for (let j = i; j < this.numberOfItems - 1; ++j, ++i) {
|
||||
newxforms[j] = this._xforms[i + 1];
|
||||
}
|
||||
this.numberOfItems--;
|
||||
this._xforms = newxforms;
|
||||
this._list._update();
|
||||
return retValue;
|
||||
}
|
||||
const err = new Error('DOMException with code=INDEX_SIZE_ERR');
|
||||
err.code = 1;
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTransform} newItem
|
||||
* @returns {SVGTransform}
|
||||
*/
|
||||
appendItem (newItem) {
|
||||
this._removeFromOtherLists(newItem);
|
||||
this._xforms.push(newItem);
|
||||
this.numberOfItems++;
|
||||
this._list._update();
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @function module:SVGTransformList.resetListMap
|
||||
* @returns {void}
|
||||
*/
|
||||
export const resetListMap = function () {
|
||||
listMap_ = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes transforms of the given element from the map.
|
||||
* @function module:SVGTransformList.removeElementFromListMap
|
||||
* @param {Element} elem - a DOM Element
|
||||
* @returns {void}
|
||||
*/
|
||||
export let removeElementFromListMap = function (elem) { // eslint-disable-line import/no-mutable-exports
|
||||
if (elem.id && listMap_[elem.id]) {
|
||||
delete listMap_[elem.id];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an object that behaves like a `SVGTransformList` for the given DOM element.
|
||||
* @function module:SVGTransformList.getTransformList
|
||||
* @param {Element} elem - DOM element to get a transformlist from
|
||||
* @todo The polyfill should have `SVGAnimatedTransformList` and this should use it
|
||||
* @returns {SVGAnimatedTransformList|SVGTransformList}
|
||||
*/
|
||||
export const getTransformList = function (elem) {
|
||||
if (!supportsNativeTransformLists()) {
|
||||
const id = elem.id || 'temp';
|
||||
let t = listMap_[id];
|
||||
if (!t || id === 'temp') {
|
||||
listMap_[id] = new SVGTransformList(elem);
|
||||
listMap_[id]._init();
|
||||
t = listMap_[id];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
if (elem.transform) {
|
||||
return elem.transform.baseVal;
|
||||
}
|
||||
if (elem.gradientTransform) {
|
||||
return elem.gradientTransform.baseVal;
|
||||
}
|
||||
if (elem.patternTransform) {
|
||||
return elem.patternTransform.baseVal;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback module:SVGTransformList.removeElementFromListMap
|
||||
* @param {Element} elem
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* Replace `removeElementFromListMap` for unit-testing.
|
||||
* @function module:SVGTransformList.changeRemoveElementFromListMap
|
||||
* @param {module:SVGTransformList.removeElementFromListMap} cb Passed a single argument `elem`
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
export const changeRemoveElementFromListMap = function (cb) { // eslint-disable-line promise/prefer-await-to-callbacks
|
||||
removeElementFromListMap = cb;
|
||||
};
|
||||
315
src/common/units.js
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Tools for working with units.
|
||||
* @module units
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
||||
*/
|
||||
|
||||
import {NS} from './namespaces.js';
|
||||
import {isNullish} from './utilities.js';
|
||||
|
||||
const wAttrs = ['x', 'x1', 'cx', 'rx', 'width'];
|
||||
const hAttrs = ['y', 'y1', 'cy', 'ry', 'height'];
|
||||
const unitAttrs = ['r', 'radius', ...wAttrs, ...hAttrs];
|
||||
// unused
|
||||
/*
|
||||
const unitNumMap = {
|
||||
'%': 2,
|
||||
em: 3,
|
||||
ex: 4,
|
||||
px: 5,
|
||||
cm: 6,
|
||||
mm: 7,
|
||||
in: 8,
|
||||
pt: 9,
|
||||
pc: 10
|
||||
};
|
||||
*/
|
||||
// Container of elements.
|
||||
let elementContainer_;
|
||||
|
||||
// Stores mapping of unit type to user coordinates.
|
||||
let typeMap_ = {};
|
||||
|
||||
/**
|
||||
* @interface module:units.ElementContainer
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getBaseUnit
|
||||
* @returns {string} The base unit type of the container ('em')
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getElement
|
||||
* @returns {?Element} An element in the container given an id
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getHeight
|
||||
* @returns {Float} The container's height
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getWidth
|
||||
* @returns {Float} The container's width
|
||||
*/
|
||||
/**
|
||||
* @function module:units.ElementContainer#getRoundDigits
|
||||
* @returns {Integer} The number of digits number should be rounded to
|
||||
*/
|
||||
|
||||
// Todo[eslint-plugin-jsdoc@>=29.0.0]: See if parsing fixed to allow '%'
|
||||
/* eslint-disable jsdoc/valid-types */
|
||||
/**
|
||||
* @typedef {PlainObject} module:units.TypeMap
|
||||
* @property {Float} em
|
||||
* @property {Float} ex
|
||||
* @property {Float} in
|
||||
* @property {Float} cm
|
||||
* @property {Float} mm
|
||||
* @property {Float} pt
|
||||
* @property {Float} pc
|
||||
* @property {Integer} px
|
||||
* @property {0} %
|
||||
*/
|
||||
/* eslint-enable jsdoc/valid-types */
|
||||
|
||||
/**
|
||||
* Initializes this module.
|
||||
*
|
||||
* @function module:units.init
|
||||
* @param {module:units.ElementContainer} elementContainer - An object implementing the ElementContainer interface.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const init = function (elementContainer) {
|
||||
elementContainer_ = elementContainer;
|
||||
|
||||
// Get correct em/ex values by creating a temporary SVG.
|
||||
const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
document.body.append(svg);
|
||||
const rect = document.createElementNS(NS.SVG, 'rect');
|
||||
rect.setAttribute('width', '1em');
|
||||
rect.setAttribute('height', '1ex');
|
||||
rect.setAttribute('x', '1in');
|
||||
svg.append(rect);
|
||||
const bb = rect.getBBox();
|
||||
svg.remove();
|
||||
|
||||
const inch = bb.x;
|
||||
typeMap_ = {
|
||||
em: bb.width,
|
||||
ex: bb.height,
|
||||
in: inch,
|
||||
cm: inch / 2.54,
|
||||
mm: inch / 25.4,
|
||||
pt: inch / 72,
|
||||
pc: inch / 6,
|
||||
px: 1,
|
||||
'%': 0
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Group: Unit conversion functions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @function module:units.getTypeMap
|
||||
* @returns {module:units.TypeMap} The unit object with values for each unit
|
||||
*/
|
||||
export const getTypeMap = function () {
|
||||
return typeMap_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {GenericArray} module:units.CompareNumbers
|
||||
* @property {Integer} length 2
|
||||
* @property {Float} 0
|
||||
* @property {Float} 1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Rounds a given value to a float with number of digits defined in
|
||||
* `round_digits` of `saveOptions`
|
||||
*
|
||||
* @function module:units.shortFloat
|
||||
* @param {string|Float|module:units.CompareNumbers} val - The value (or Array of two numbers) to be rounded
|
||||
* @returns {Float|string} If a string/number was given, returns a Float. If an array, return a string
|
||||
* with comma-separated floats
|
||||
*/
|
||||
export const shortFloat = function (val) {
|
||||
const digits = elementContainer_.getRoundDigits();
|
||||
if (!isNaN(val)) {
|
||||
return Number(Number(val).toFixed(digits));
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return shortFloat(val[0]) + ',' + shortFloat(val[1]);
|
||||
}
|
||||
return Number.parseFloat(val).toFixed(digits) - 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the number to given unit or baseUnit.
|
||||
* @function module:units.convertUnit
|
||||
* @param {string|Float} val
|
||||
* @param {"em"|"ex"|"in"|"cm"|"mm"|"pt"|"pc"|"px"|"%"} [unit]
|
||||
* @returns {Float}
|
||||
*/
|
||||
export const convertUnit = function (val, unit) {
|
||||
unit = unit || elementContainer_.getBaseUnit();
|
||||
// baseVal.convertToSpecifiedUnits(unitNumMap[unit]);
|
||||
// const val = baseVal.valueInSpecifiedUnits;
|
||||
// baseVal.convertToSpecifiedUnits(1);
|
||||
return shortFloat(val / typeMap_[unit]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets an element's attribute based on the unit in its current value.
|
||||
*
|
||||
* @function module:units.setUnitAttr
|
||||
* @param {Element} elem - DOM element to be changed
|
||||
* @param {string} attr - Name of the attribute associated with the value
|
||||
* @param {string} val - Attribute value to convert
|
||||
* @returns {void}
|
||||
*/
|
||||
export const setUnitAttr = function (elem, attr, val) {
|
||||
// if (!isNaN(val)) {
|
||||
// New value is a number, so check currently used unit
|
||||
// const oldVal = elem.getAttribute(attr);
|
||||
|
||||
// Enable this for alternate mode
|
||||
// if (oldVal !== null && (isNaN(oldVal) || elementContainer_.getBaseUnit() !== 'px')) {
|
||||
// // Old value was a number, so get unit, then convert
|
||||
// let unit;
|
||||
// if (oldVal.substr(-1) === '%') {
|
||||
// const res = getResolution();
|
||||
// unit = '%';
|
||||
// val *= 100;
|
||||
// if (wAttrs.includes(attr)) {
|
||||
// val = val / res.w;
|
||||
// } else if (hAttrs.includes(attr)) {
|
||||
// val = val / res.h;
|
||||
// } else {
|
||||
// return val / Math.sqrt((res.w*res.w) + (res.h*res.h))/Math.sqrt(2);
|
||||
// }
|
||||
// } else {
|
||||
// if (elementContainer_.getBaseUnit() !== 'px') {
|
||||
// unit = elementContainer_.getBaseUnit();
|
||||
// } else {
|
||||
// unit = oldVal.substr(-2);
|
||||
// }
|
||||
// val = val / typeMap_[unit];
|
||||
// }
|
||||
//
|
||||
// val += unit;
|
||||
// }
|
||||
// }
|
||||
elem.setAttribute(attr, val);
|
||||
};
|
||||
|
||||
const attrsToConvert = {
|
||||
line: ['x1', 'x2', 'y1', 'y2'],
|
||||
circle: ['cx', 'cy', 'r'],
|
||||
ellipse: ['cx', 'cy', 'rx', 'ry'],
|
||||
foreignObject: ['x', 'y', 'width', 'height'],
|
||||
rect: ['x', 'y', 'width', 'height'],
|
||||
image: ['x', 'y', 'width', 'height'],
|
||||
use: ['x', 'y', 'width', 'height'],
|
||||
text: ['x', 'y']
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts all applicable attributes to the configured baseUnit.
|
||||
* @function module:units.convertAttrs
|
||||
* @param {Element} element - A DOM element whose attributes should be converted
|
||||
* @returns {void}
|
||||
*/
|
||||
export const convertAttrs = function (element) {
|
||||
const elName = element.tagName;
|
||||
const unit = elementContainer_.getBaseUnit();
|
||||
const attrs = attrsToConvert[elName];
|
||||
if (!attrs) { return; }
|
||||
|
||||
const len = attrs.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const attr = attrs[i];
|
||||
const cur = element.getAttribute(attr);
|
||||
if (cur) {
|
||||
if (!isNaN(cur)) {
|
||||
element.setAttribute(attr, (cur / typeMap_[unit]) + unit);
|
||||
}
|
||||
// else {
|
||||
// Convert existing?
|
||||
// }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts given values to numbers. Attributes must be supplied in
|
||||
* case a percentage is given.
|
||||
*
|
||||
* @function module:units.convertToNum
|
||||
* @param {string} attr - Name of the attribute associated with the value
|
||||
* @param {string} val - Attribute value to convert
|
||||
* @returns {Float} The converted number
|
||||
*/
|
||||
export const convertToNum = function (attr, val) {
|
||||
// Return a number if that's what it already is
|
||||
if (!isNaN(val)) { return val - 0; }
|
||||
if (val.substr(-1) === '%') {
|
||||
// Deal with percentage, depends on attribute
|
||||
const num = val.substr(0, val.length - 1) / 100;
|
||||
const width = elementContainer_.getWidth();
|
||||
const height = elementContainer_.getHeight();
|
||||
|
||||
if (wAttrs.includes(attr)) {
|
||||
return num * width;
|
||||
}
|
||||
if (hAttrs.includes(attr)) {
|
||||
return num * height;
|
||||
}
|
||||
return num * Math.sqrt((width * width) + (height * height)) / Math.sqrt(2);
|
||||
}
|
||||
const unit = val.substr(-2);
|
||||
const num = val.substr(0, val.length - 2);
|
||||
// Note that this multiplication turns the string into a number
|
||||
return num * typeMap_[unit];
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an attribute's value is in a valid format.
|
||||
* @function module:units.isValidUnit
|
||||
* @param {string} attr - The name of the attribute associated with the value
|
||||
* @param {string} val - The attribute value to check
|
||||
* @param {Element} selectedElement
|
||||
* @returns {boolean} Whether the unit is valid
|
||||
*/
|
||||
export const isValidUnit = function (attr, val, selectedElement) {
|
||||
if (unitAttrs.includes(attr)) {
|
||||
// True if it's just a number
|
||||
if (!isNaN(val)) {
|
||||
return true;
|
||||
}
|
||||
// Not a number, check if it has a valid unit
|
||||
val = val.toLowerCase();
|
||||
return Object.keys(typeMap_).some((unit) => {
|
||||
const re = new RegExp('^-?[\\d\\.]+' + unit + '$');
|
||||
return re.test(val);
|
||||
});
|
||||
}
|
||||
if (attr === 'id') {
|
||||
// if we're trying to change the id, make sure it's not already present in the doc
|
||||
// and the id value is valid.
|
||||
|
||||
let result = false;
|
||||
// because getElem() can throw an exception in the case of an invalid id
|
||||
// (according to https://www.w3.org/TR/xml-id/ IDs must be a NCName)
|
||||
// we wrap it in an exception and only return true if the ID was valid and
|
||||
// not already present
|
||||
try {
|
||||
const elem = elementContainer_.getElement(val);
|
||||
result = (isNullish(elem) || elem === selectedElement);
|
||||
} catch (e) {}
|
||||
return result;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
1362
src/common/utilities.js
Normal file
54
src/editor/browser-not-supported.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
|
||||
<link rel="icon" type="image/png" href="images/logo.png"/>
|
||||
<link rel="stylesheet" href="svg-editor.css"/>
|
||||
<title>Browser does not support SVG | SVG-edit</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
font-family: Verdana, Helvetica, Arial;
|
||||
color: #000;
|
||||
padding: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
#logo {
|
||||
float: left;
|
||||
padding: 10px;
|
||||
}
|
||||
#caniuse {
|
||||
position: absolute;
|
||||
top: 7em;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#caniuse > iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img id="logo" src="images/logo.png" width="48" height="48" alt="SVG-edit logo" />
|
||||
<p>Sorry, but your browser does not support SVG. Below is a list of
|
||||
alternate browsers and versions that support SVG and SVG-edit
|
||||
(from <a href="https://caniuse.com/#cats=SVG">caniuse.com</a>).
|
||||
</p>
|
||||
<p>Try the latest version of
|
||||
<a href="https://www.getfirefox.com">Firefox</a>,
|
||||
<a href="https://www.google.com/chrome/">Chrome</a>,
|
||||
<a href="https://www.apple.com/safari/">Safari</a>,
|
||||
<a href="https://www.opera.com/download">Opera</a> or
|
||||
<a href="https://support.microsoft.com/en-us/help/17621/internet-explorer-downloads">Internet Explorer</a>.
|
||||
</p>
|
||||
<div id="caniuse">
|
||||
<iframe src="https://caniuse.com/#cats=SVG"></iframe>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2995
src/editor/canvg/canvg.js
Normal file
282
src/editor/canvg/rgbcolor.js
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* For parsing color values.
|
||||
* @module RGBColor
|
||||
* @author Stoyan Stefanov <sstoo@gmail.com>
|
||||
* @see https://www.phpied.com/rgb-color-parser-in-javascript/
|
||||
* @license MIT
|
||||
*/
|
||||
const simpleColors = {
|
||||
aliceblue: 'f0f8ff',
|
||||
antiquewhite: 'faebd7',
|
||||
aqua: '00ffff',
|
||||
aquamarine: '7fffd4',
|
||||
azure: 'f0ffff',
|
||||
beige: 'f5f5dc',
|
||||
bisque: 'ffe4c4',
|
||||
black: '000000',
|
||||
blanchedalmond: 'ffebcd',
|
||||
blue: '0000ff',
|
||||
blueviolet: '8a2be2',
|
||||
brown: 'a52a2a',
|
||||
burlywood: 'deb887',
|
||||
cadetblue: '5f9ea0',
|
||||
chartreuse: '7fff00',
|
||||
chocolate: 'd2691e',
|
||||
coral: 'ff7f50',
|
||||
cornflowerblue: '6495ed',
|
||||
cornsilk: 'fff8dc',
|
||||
crimson: 'dc143c',
|
||||
cyan: '00ffff',
|
||||
darkblue: '00008b',
|
||||
darkcyan: '008b8b',
|
||||
darkgoldenrod: 'b8860b',
|
||||
darkgray: 'a9a9a9',
|
||||
darkgreen: '006400',
|
||||
darkkhaki: 'bdb76b',
|
||||
darkmagenta: '8b008b',
|
||||
darkolivegreen: '556b2f',
|
||||
darkorange: 'ff8c00',
|
||||
darkorchid: '9932cc',
|
||||
darkred: '8b0000',
|
||||
darksalmon: 'e9967a',
|
||||
darkseagreen: '8fbc8f',
|
||||
darkslateblue: '483d8b',
|
||||
darkslategray: '2f4f4f',
|
||||
darkturquoise: '00ced1',
|
||||
darkviolet: '9400d3',
|
||||
deeppink: 'ff1493',
|
||||
deepskyblue: '00bfff',
|
||||
dimgray: '696969',
|
||||
dodgerblue: '1e90ff',
|
||||
feldspar: 'd19275',
|
||||
firebrick: 'b22222',
|
||||
floralwhite: 'fffaf0',
|
||||
forestgreen: '228b22',
|
||||
fuchsia: 'ff00ff',
|
||||
gainsboro: 'dcdcdc',
|
||||
ghostwhite: 'f8f8ff',
|
||||
gold: 'ffd700',
|
||||
goldenrod: 'daa520',
|
||||
gray: '808080',
|
||||
green: '008000',
|
||||
greenyellow: 'adff2f',
|
||||
honeydew: 'f0fff0',
|
||||
hotpink: 'ff69b4',
|
||||
indianred: 'cd5c5c',
|
||||
indigo: '4b0082',
|
||||
ivory: 'fffff0',
|
||||
khaki: 'f0e68c',
|
||||
lavender: 'e6e6fa',
|
||||
lavenderblush: 'fff0f5',
|
||||
lawngreen: '7cfc00',
|
||||
lemonchiffon: 'fffacd',
|
||||
lightblue: 'add8e6',
|
||||
lightcoral: 'f08080',
|
||||
lightcyan: 'e0ffff',
|
||||
lightgoldenrodyellow: 'fafad2',
|
||||
lightgrey: 'd3d3d3',
|
||||
lightgreen: '90ee90',
|
||||
lightpink: 'ffb6c1',
|
||||
lightsalmon: 'ffa07a',
|
||||
lightseagreen: '20b2aa',
|
||||
lightskyblue: '87cefa',
|
||||
lightslateblue: '8470ff',
|
||||
lightslategray: '778899',
|
||||
lightsteelblue: 'b0c4de',
|
||||
lightyellow: 'ffffe0',
|
||||
lime: '00ff00',
|
||||
limegreen: '32cd32',
|
||||
linen: 'faf0e6',
|
||||
magenta: 'ff00ff',
|
||||
maroon: '800000',
|
||||
mediumaquamarine: '66cdaa',
|
||||
mediumblue: '0000cd',
|
||||
mediumorchid: 'ba55d3',
|
||||
mediumpurple: '9370d8',
|
||||
mediumseagreen: '3cb371',
|
||||
mediumslateblue: '7b68ee',
|
||||
mediumspringgreen: '00fa9a',
|
||||
mediumturquoise: '48d1cc',
|
||||
mediumvioletred: 'c71585',
|
||||
midnightblue: '191970',
|
||||
mintcream: 'f5fffa',
|
||||
mistyrose: 'ffe4e1',
|
||||
moccasin: 'ffe4b5',
|
||||
navajowhite: 'ffdead',
|
||||
navy: '000080',
|
||||
oldlace: 'fdf5e6',
|
||||
olive: '808000',
|
||||
olivedrab: '6b8e23',
|
||||
orange: 'ffa500',
|
||||
orangered: 'ff4500',
|
||||
orchid: 'da70d6',
|
||||
palegoldenrod: 'eee8aa',
|
||||
palegreen: '98fb98',
|
||||
paleturquoise: 'afeeee',
|
||||
palevioletred: 'd87093',
|
||||
papayawhip: 'ffefd5',
|
||||
peachpuff: 'ffdab9',
|
||||
peru: 'cd853f',
|
||||
pink: 'ffc0cb',
|
||||
plum: 'dda0dd',
|
||||
powderblue: 'b0e0e6',
|
||||
purple: '800080',
|
||||
red: 'ff0000',
|
||||
rosybrown: 'bc8f8f',
|
||||
royalblue: '4169e1',
|
||||
saddlebrown: '8b4513',
|
||||
salmon: 'fa8072',
|
||||
sandybrown: 'f4a460',
|
||||
seagreen: '2e8b57',
|
||||
seashell: 'fff5ee',
|
||||
sienna: 'a0522d',
|
||||
silver: 'c0c0c0',
|
||||
skyblue: '87ceeb',
|
||||
slateblue: '6a5acd',
|
||||
slategray: '708090',
|
||||
snow: 'fffafa',
|
||||
springgreen: '00ff7f',
|
||||
steelblue: '4682b4',
|
||||
tan: 'd2b48c',
|
||||
teal: '008080',
|
||||
thistle: 'd8bfd8',
|
||||
tomato: 'ff6347',
|
||||
turquoise: '40e0d0',
|
||||
violet: 'ee82ee',
|
||||
violetred: 'd02090',
|
||||
wheat: 'f5deb3',
|
||||
white: 'ffffff',
|
||||
whitesmoke: 'f5f5f5',
|
||||
yellow: 'ffff00',
|
||||
yellowgreen: '9acd32'
|
||||
};
|
||||
|
||||
// array of color definition objects
|
||||
const colorDefs = [
|
||||
{
|
||||
re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
|
||||
// re: /^rgb\((?<r>\d{1,3}),\s*(?<g>\d{1,3}),\s*(?<b>\d{1,3})\)$/,
|
||||
example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
|
||||
process (_, ...bits) {
|
||||
return bits.map((b) => Number.parseInt(b));
|
||||
}
|
||||
},
|
||||
{
|
||||
re: /^(\w{2})(\w{2})(\w{2})$/,
|
||||
// re: /^(?<r>\w{2})(?<g>\w{2})(?<b>\w{2})$/,
|
||||
example: ['#00ff00', '336699'],
|
||||
process (_, ...bits) {
|
||||
return bits.map((b) => Number.parseInt(b, 16));
|
||||
}
|
||||
},
|
||||
{
|
||||
re: /^(\w)(\w)(\w)$/,
|
||||
// re: /^(?<r>\w{1})(?<g>\w{1})(?<b>\w{1})$/,
|
||||
example: ['#fb0', 'f0f'],
|
||||
process (_, ...bits) {
|
||||
return bits.map((b) => Number.parseInt(b + b, 16));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* A class to parse color values.
|
||||
*/
|
||||
export default class RGBColor {
|
||||
/**
|
||||
* @param {string} colorString
|
||||
*/
|
||||
constructor (colorString) {
|
||||
this.ok = false;
|
||||
|
||||
// strip any leading #
|
||||
if (colorString.charAt(0) === '#') { // remove # if any
|
||||
colorString = colorString.substr(1, 6);
|
||||
}
|
||||
|
||||
colorString = colorString.replace(/ /g, '');
|
||||
colorString = colorString.toLowerCase();
|
||||
|
||||
// before getting into regexps, try simple matches
|
||||
// and overwrite the input
|
||||
if (colorString in simpleColors) {
|
||||
colorString = simpleColors[colorString];
|
||||
}
|
||||
// end of simple type-in colors
|
||||
|
||||
// search through the definitions to find a match
|
||||
|
||||
colorDefs.forEach(({re, process: processor}) => {
|
||||
const bits = re.exec(colorString);
|
||||
if (bits) {
|
||||
const [r, g, b] = processor(...bits);
|
||||
Object.assign(this, {r, g, b});
|
||||
this.ok = true;
|
||||
}
|
||||
});
|
||||
|
||||
// validate/cleanup values
|
||||
this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
|
||||
this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
|
||||
this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
|
||||
}
|
||||
|
||||
// some getters
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
toRGB () {
|
||||
return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
toHex () {
|
||||
let r = this.r.toString(16);
|
||||
let g = this.g.toString(16);
|
||||
let b = this.b.toString(16);
|
||||
if (r.length === 1) { r = '0' + r; }
|
||||
if (g.length === 1) { g = '0' + g; }
|
||||
if (b.length === 1) { b = '0' + b; }
|
||||
return '#' + r + g + b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offers a bulleted list of help.
|
||||
* @returns {HTMLUListElement}
|
||||
*/
|
||||
static getHelpXML () {
|
||||
const examples = [
|
||||
// add regexps
|
||||
...colorDefs.flatMap(({example}) => {
|
||||
return example;
|
||||
}),
|
||||
// add type-in colors
|
||||
...Object.keys(simpleColors)
|
||||
];
|
||||
|
||||
const xml = document.createElement('ul');
|
||||
xml.setAttribute('id', 'rgbcolor-examples');
|
||||
|
||||
xml.append(...examples.map((example) => {
|
||||
try {
|
||||
const listItem = document.createElement('li');
|
||||
const listColor = new RGBColor(example);
|
||||
const exampleDiv = document.createElement('div');
|
||||
exampleDiv.style.cssText = `
|
||||
margin: 3px;
|
||||
border: 1px solid black;
|
||||
background: ${listColor.toHex()};
|
||||
color: ${listColor.toHex()};`;
|
||||
exampleDiv.append('test');
|
||||
const listItemValue = ` ${example} -> ${listColor.toRGB()} -> ${listColor.toHex()}`;
|
||||
listItem.append(exampleDiv, listItemValue);
|
||||
return listItem;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}));
|
||||
return xml;
|
||||
}
|
||||
}
|
||||
105
src/editor/contextmenu.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/* globals jQuery */
|
||||
/**
|
||||
* Adds context menu functionality.
|
||||
* @module contextmenu
|
||||
* @license Apache-2.0
|
||||
* @author Adam Bender
|
||||
*/
|
||||
// Dependencies:
|
||||
// 1) jQuery (for dom injection of context menus)
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
let contextMenuExtensions = {};
|
||||
|
||||
/**
|
||||
* Signature depends on what the user adds; in the case of our uses with
|
||||
* SVGEditor, no parameters are passed nor anything expected for a return.
|
||||
* @callback module:contextmenu.MenuItemAction
|
||||
* @param {...args} args
|
||||
* @returns {any}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:contextmenu.MenuItem
|
||||
* @property {string} id
|
||||
* @property {string} label
|
||||
* @property {module:contextmenu.MenuItemAction} action
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {module:contextmenu.MenuItem} menuItem
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const menuItemIsValid = function (menuItem) {
|
||||
return menuItem && menuItem.id && menuItem.label && menuItem.action && typeof menuItem.action === 'function';
|
||||
};
|
||||
|
||||
/**
|
||||
* @function module:contextmenu.add
|
||||
* @param {module:contextmenu.MenuItem} menuItem
|
||||
* @throws {Error|TypeError}
|
||||
* @returns {void}
|
||||
*/
|
||||
export const add = function (menuItem) {
|
||||
// menuItem: {id, label, shortcut, action}
|
||||
if (!menuItemIsValid(menuItem)) {
|
||||
throw new TypeError('Menu items must be defined and have at least properties: id, label, action, where action must be a function');
|
||||
}
|
||||
if (menuItem.id in contextMenuExtensions) {
|
||||
throw new Error('Cannot add extension "' + menuItem.id + '", an extension by that name already exists"');
|
||||
}
|
||||
// Register menuItem action, see below for deferred menu dom injection
|
||||
console.log('Registered contextmenu item: {id:' + menuItem.id + ', label:' + menuItem.label + '}'); // eslint-disable-line no-console
|
||||
contextMenuExtensions[menuItem.id] = menuItem;
|
||||
// TODO: Need to consider how to handle custom enable/disable behavior
|
||||
};
|
||||
|
||||
/**
|
||||
* @function module:contextmenu.hasCustomHandler
|
||||
* @param {string} handlerKey
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const hasCustomHandler = function (handlerKey) {
|
||||
return Boolean(contextMenuExtensions[handlerKey]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @function module:contextmenu.getCustomHandler
|
||||
* @param {string} handlerKey
|
||||
* @returns {module:contextmenu.MenuItemAction}
|
||||
*/
|
||||
export const getCustomHandler = function (handlerKey) {
|
||||
return contextMenuExtensions[handlerKey].action;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:contextmenu.MenuItem} menuItem
|
||||
* @returns {void}
|
||||
*/
|
||||
const injectExtendedContextMenuItemIntoDom = function (menuItem) {
|
||||
if (!Object.keys(contextMenuExtensions).length) {
|
||||
// all menuItems appear at the bottom of the menu in their own container.
|
||||
// if this is the first extension menu we need to add the separator.
|
||||
$('#cmenu_canvas').append("<li class='separator'>");
|
||||
}
|
||||
const shortcut = menuItem.shortcut || '';
|
||||
$('#cmenu_canvas').append("<li class='disabled'><a href='#" + menuItem.id + "'>" +
|
||||
menuItem.label + "<span class='shortcut'>" +
|
||||
shortcut + '</span></a></li>');
|
||||
};
|
||||
|
||||
/**
|
||||
* @function module:contextmenu.injectExtendedContextMenuItemsIntoDom
|
||||
* @returns {void}
|
||||
*/
|
||||
export const injectExtendedContextMenuItemsIntoDom = function () {
|
||||
Object.values(contextMenuExtensions).forEach((menuItem) => {
|
||||
injectExtendedContextMenuItemIntoDom(menuItem);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* @function module:contextmenu.resetCustomMenus
|
||||
* @returns {void}
|
||||
*/
|
||||
export const resetCustomMenus = function () { contextMenuExtensions = {}; };
|
||||
269
src/editor/contextmenu/jQuery.contextMenu.js
Executable file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* @file jQuery Context Menu Plugin
|
||||
* Cory S.N. LaViska
|
||||
* A Beautiful Site ({@link https://abeautifulsite.net/})
|
||||
* Modified by Alexis Deveria
|
||||
*
|
||||
* More info: {@link https://abeautifulsite.net/2008/09/jquery-context-menu-plugin/}
|
||||
*
|
||||
* @module jQueryContextMenu
|
||||
* @todo Update to latest version and adapt (and needs jQuery update as well): {@link https://github.com/swisnl/jQuery-contextMenu}
|
||||
* @version 1.0.1
|
||||
*
|
||||
* @license (MIT OR GPL-2.0-or-later)
|
||||
*
|
||||
* This plugin is dual-licensed under the GNU General Public License
|
||||
* and the MIT License and is copyright A Beautiful Site, LLC.
|
||||
*
|
||||
*/
|
||||
import {isMac} from '../../common/browser.js';
|
||||
|
||||
/**
|
||||
* @callback module:jQueryContextMenu.jQueryContextMenuListener
|
||||
* @param {string} href The `href` value after the first character (for bypassing an initial `#`)
|
||||
* @param {external:jQuery} srcElement The wrapped jQuery srcElement
|
||||
* @param {{x: Float, y: Float, docX: Float, docY: Float}} coords
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:jQueryContextMenu.jQueryContextMenuConfig
|
||||
* @property {string} menu
|
||||
* @property {Float} inSpeed
|
||||
* @property {Float} outSpeed
|
||||
* @property {boolean} allowLeft
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds {@link external:jQuery.fn.contextMenu},
|
||||
* {@link external:jQuery.fn.disableContextMenuItems},
|
||||
* {@link external:jQuery.fn.enableContextMenuItems},
|
||||
* {@link external:jQuery.fn.disableContextMenu},
|
||||
* {@link external:jQuery.fn.enableContextMenu},
|
||||
* {@link external:jQuery.fn.destroyContextMenu}.
|
||||
* @function module:jQueryContextMenu.jQueryContextMenu
|
||||
* @param {external:jQuery} $ The jQuery object to wrap (with `contextMenu`, `disableContextMenuItems`, `enableContextMenuItems`, `disableContextMenu`, `enableContextMenu`, `destroyContextMenu`)
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
function jQueryContextMenu ($) {
|
||||
const win = $(window);
|
||||
const doc = $(document);
|
||||
|
||||
$.extend($.fn, {
|
||||
/**
|
||||
* @memberof external:jQuery.fn
|
||||
* @param {module:jQueryContextMenu.jQueryContextMenuConfig} o
|
||||
* @param {module:jQueryContextMenu.jQueryContextMenuListener} listener
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
contextMenu (o, listener) {
|
||||
// Defaults
|
||||
if (o.menu === undefined) return false;
|
||||
if (o.inSpeed === undefined) o.inSpeed = 150;
|
||||
if (o.outSpeed === undefined) o.outSpeed = 75;
|
||||
// 0 needs to be -1 for expected results (no fade)
|
||||
if (o.inSpeed === 0) o.inSpeed = -1;
|
||||
if (o.outSpeed === 0) o.outSpeed = -1;
|
||||
// Loop each context menu
|
||||
$(this).each(function () {
|
||||
const el = $(this);
|
||||
const offset = $(el).offset();
|
||||
|
||||
const menu = $('#' + o.menu);
|
||||
|
||||
// Add contextMenu class
|
||||
menu.addClass('contextMenu');
|
||||
// Simulate a true right click
|
||||
$(this).bind('mousedown', function (evt) {
|
||||
$(this).mouseup(function (e) {
|
||||
const srcElement = $(this);
|
||||
srcElement.unbind('mouseup');
|
||||
|
||||
if (!(evt.button === 2 || o.allowLeft ||
|
||||
(evt.ctrlKey && isMac()))) {
|
||||
return undefined;
|
||||
}
|
||||
e.stopPropagation();
|
||||
// Hide context menus that may be showing
|
||||
$('.contextMenu').hide();
|
||||
// Get this context menu
|
||||
|
||||
if (el.hasClass('disabled')) return false;
|
||||
|
||||
// Detect mouse position
|
||||
let x = e.pageX, y = e.pageY;
|
||||
|
||||
const xOff = win.width() - menu.width(),
|
||||
yOff = win.height() - menu.height();
|
||||
|
||||
if (x > xOff - 15) x = xOff - 15;
|
||||
if (y > yOff - 30) y = yOff - 30; // 30 is needed to prevent scrollbars in FF
|
||||
|
||||
// Show the menu
|
||||
doc.unbind('click');
|
||||
menu.css({top: y, left: x}).fadeIn(o.inSpeed);
|
||||
// Hover events
|
||||
menu.find('A').mouseover(function () {
|
||||
menu.find('LI.hover').removeClass('hover');
|
||||
$(this).parent().addClass('hover');
|
||||
}).mouseout(function () {
|
||||
menu.find('LI.hover').removeClass('hover');
|
||||
});
|
||||
|
||||
// Keyboard
|
||||
doc.keypress(function (ev) {
|
||||
switch (ev.keyCode) {
|
||||
case 38: // up
|
||||
if (!menu.find('LI.hover').length) {
|
||||
menu.find('LI:last').addClass('hover');
|
||||
} else {
|
||||
menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover');
|
||||
if (!menu.find('LI.hover').length) menu.find('LI:last').addClass('hover');
|
||||
}
|
||||
break;
|
||||
case 40: // down
|
||||
if (!menu.find('LI.hover').length) {
|
||||
menu.find('LI:first').addClass('hover');
|
||||
} else {
|
||||
menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover');
|
||||
if (!menu.find('LI.hover').length) menu.find('LI:first').addClass('hover');
|
||||
}
|
||||
break;
|
||||
case 13: // enter
|
||||
menu.find('LI.hover A').trigger('click');
|
||||
break;
|
||||
case 27: // esc
|
||||
doc.trigger('click');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// When items are selected
|
||||
menu.find('A').unbind('mouseup');
|
||||
menu.find('LI:not(.disabled) A').mouseup(function () {
|
||||
doc.unbind('click').unbind('keypress');
|
||||
$('.contextMenu').hide();
|
||||
if (listener) {
|
||||
listener($(this).attr('href').substr(1), $(srcElement), {
|
||||
x: x - offset.left, y: y - offset.top, docX: x, docY: y
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Hide bindings
|
||||
setTimeout(function () { // Delay for Mozilla
|
||||
doc.click(function () {
|
||||
doc.unbind('click').unbind('keypress');
|
||||
menu.fadeOut(o.outSpeed);
|
||||
return false;
|
||||
});
|
||||
}, 0);
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
// Disable text selection
|
||||
if ($.browser.mozilla) {
|
||||
$('#' + o.menu).each(function () { $(this).css({MozUserSelect: 'none'}); });
|
||||
} else if ($.browser.msie) {
|
||||
$('#' + o.menu).each(function () { $(this).bind('selectstart.disableTextSelect', function () { return false; }); });
|
||||
} else {
|
||||
$('#' + o.menu).each(function () { $(this).bind('mousedown.disableTextSelect', function () { return false; }); });
|
||||
}
|
||||
// Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome)
|
||||
$(el).add($('UL.contextMenu')).bind('contextmenu', function () { return false; });
|
||||
});
|
||||
return $(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable context menu items on the fly.
|
||||
* @memberof external:jQuery.fn
|
||||
* @param {void|string} o Comma-separated
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
disableContextMenuItems (o) {
|
||||
if (o === undefined) {
|
||||
// Disable all
|
||||
$(this).find('LI').addClass('disabled');
|
||||
return $(this);
|
||||
}
|
||||
$(this).each(function () {
|
||||
if (o !== undefined) {
|
||||
const d = o.split(',');
|
||||
for (const href of d) {
|
||||
// eslint-disable-next-line unicorn/no-fn-reference-in-iterator
|
||||
$(this).find('A[href="' + href + '"]').parent().addClass('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
return $(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable context menu items on the fly.
|
||||
* @memberof external:jQuery.fn
|
||||
* @param {void|string} o Comma-separated
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
enableContextMenuItems (o) {
|
||||
if (o === undefined) {
|
||||
// Enable all
|
||||
$(this).find('LI.disabled').removeClass('disabled');
|
||||
return $(this);
|
||||
}
|
||||
$(this).each(function () {
|
||||
if (o !== undefined) {
|
||||
const d = o.split(',');
|
||||
for (const href of d) {
|
||||
// eslint-disable-next-line unicorn/no-fn-reference-in-iterator
|
||||
$(this).find('A[href="' + href + '"]').parent().removeClass('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
return $(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable context menu(s).
|
||||
* @memberof external:jQuery.fn
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
disableContextMenu () {
|
||||
$(this).each(function () {
|
||||
$(this).addClass('disabled');
|
||||
});
|
||||
return $(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable context menu(s).
|
||||
* @memberof external:jQuery.fn
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
enableContextMenu () {
|
||||
$(this).each(function () {
|
||||
$(this).removeClass('disabled');
|
||||
});
|
||||
return $(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy context menu(s).
|
||||
* @memberof external:jQuery.fn
|
||||
* @returns {external:jQuery}
|
||||
*/
|
||||
destroyContextMenu () {
|
||||
// Destroy specified context menus
|
||||
$(this).each(function () {
|
||||
// Disable action
|
||||
$(this).unbind('mousedown').unbind('mouseup');
|
||||
});
|
||||
return $(this);
|
||||
}
|
||||
});
|
||||
return $;
|
||||
}
|
||||
|
||||
export default jQueryContextMenu;
|
||||
125
src/editor/embedapi-dom.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/* globals jQuery */
|
||||
/**
|
||||
* Attaches items to DOM for Embedded SVG support.
|
||||
* @module EmbeddedSVGEditDOM
|
||||
*/
|
||||
import EmbeddedSVGEdit from './embedapi.js';
|
||||
import {isChrome} from './browser.js';
|
||||
|
||||
const $ = jQuery;
|
||||
|
||||
let svgCanvas = null;
|
||||
|
||||
/**
|
||||
* @param {string} data
|
||||
* @param {string} error
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleSvgData (data, error) {
|
||||
if (error) {
|
||||
// Todo: This should be replaced with a general purpose dialog alert library call
|
||||
alert('error ' + error); // eslint-disable-line no-alert
|
||||
} else {
|
||||
// Todo: This should be replaced with a general purpose dialog alert library call
|
||||
alert('Congratulations. Your SVG string is back in the host page, do with it what you will\n\n' + data); // eslint-disable-line no-alert
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the canvas with an example SVG string.
|
||||
* @returns {void}
|
||||
*/
|
||||
function loadSvg () {
|
||||
const svgexample = '<svg width="640" height="480" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><g><title>Layer 1</title><rect stroke-width="5" stroke="#000000" fill="#FF0000" id="svg_1" height="35" width="51" y="35" x="32"/><ellipse ry="15" rx="24" stroke-width="5" stroke="#000000" fill="#0000ff" id="svg_2" cy="60" cx="66"/></g></svg>';
|
||||
svgCanvas.setSvgString(svgexample);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function saveSvg () {
|
||||
svgCanvas.getSvgString()(handleSvgData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a PNG export.
|
||||
* @returns {void}
|
||||
*/
|
||||
function exportPNG () {
|
||||
svgCanvas.getUIStrings()(function (uiStrings) {
|
||||
const str = uiStrings.notification.loadingImage;
|
||||
let exportWindow;
|
||||
if (!isChrome()) {
|
||||
exportWindow = window.open(
|
||||
'data:text/html;charset=utf-8,' + encodeURIComponent('<title>' + str + '</title><h1>' + str + '</h1>'),
|
||||
'svg-edit-exportWindow'
|
||||
);
|
||||
}
|
||||
svgCanvas.rasterExport('PNG', null, exportWindow && exportWindow.name)();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a PDF export.
|
||||
* @returns {void}
|
||||
*/
|
||||
function exportPDF () {
|
||||
svgCanvas.getUIStrings()(function (uiStrings) {
|
||||
const str = uiStrings.notification.loadingImage;
|
||||
|
||||
/*
|
||||
// If you want to handle the PDF blob yourself, do as follows
|
||||
svgCanvas.bind('exportedPDF', function (win, data) {
|
||||
alert(data.output);
|
||||
});
|
||||
svgCanvas.exportPDF(); // Accepts two args: optionalWindowName supplied back to bound exportPDF handler and optional outputType (defaults to dataurlstring)
|
||||
return;
|
||||
*/
|
||||
|
||||
if (isChrome()) {
|
||||
// Chrome will open an extra window if we follow the approach below
|
||||
svgCanvas.exportPDF();
|
||||
} else {
|
||||
const exportWindow = window.open(
|
||||
'data:text/html;charset=utf-8,' + encodeURIComponent('<title>' + str + '</title><h1>' + str + '</h1>'),
|
||||
'svg-edit-exportWindow'
|
||||
);
|
||||
svgCanvas.exportPDF(exportWindow && exportWindow.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const frameBase = 'https://raw.githack.com/SVG-Edit/svgedit/master';
|
||||
// const frameBase = 'http://localhost:8001';
|
||||
const framePath = '/editor/xdomain-svg-editor-es.html?extensions=ext-xdomain-messaging.js';
|
||||
const iframe = $('<iframe width="900px" height="600px" id="svgedit" src="javascript:0"></iframe>');
|
||||
iframe[0].src = frameBase + framePath +
|
||||
(location.href.includes('?')
|
||||
// ? location.href.replace(/\?(?<search>.*)$/, '&$<search>')
|
||||
? location.href.replace(/\?(.*)$/, '&$1')
|
||||
: ''); // Append arguments to this file onto the iframe
|
||||
|
||||
iframe[0].addEventListener('load', function () {
|
||||
svgCanvas = new EmbeddedSVGEdit(frame, [new URL(frameBase).origin]);
|
||||
// Hide main button, as we will be controlling new, load, save, etc. from the host document
|
||||
let doc;
|
||||
try {
|
||||
doc = frame.contentDocument || frame.contentWindow.document;
|
||||
} catch (err) {
|
||||
console.log('Blocked from accessing document', err); // eslint-disable-line no-console
|
||||
}
|
||||
if (doc) {
|
||||
// Todo: Provide a way to get this to occur by `postMessage`
|
||||
const mainButton = doc.getElementById('main_button');
|
||||
mainButton.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add event handlers now that `svgCanvas` is ready
|
||||
$('#load').click(loadSvg);
|
||||
$('#save').click(saveSvg);
|
||||
$('#exportPNG').click(exportPNG);
|
||||
$('#exportPDF').click(exportPDF);
|
||||
});
|
||||
$('body').append(iframe);
|
||||
const frame = document.getElementById('svgedit');
|
||||
17
src/editor/embedapi.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Embed API</title>
|
||||
<link rel="icon" type="image/png" href="images/logo.png"/>
|
||||
<script src="jquery.min.js"></script>
|
||||
<script type="module" src="embedapi-dom.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="load">Load example</button>
|
||||
<button id="save">Save data</button>
|
||||
<button id="exportPNG">Export data to PNG</button>
|
||||
<button id="exportPDF">Export data to PDF</button>
|
||||
<br/>
|
||||
</body>
|
||||
</html>
|
||||
391
src/editor/embedapi.js
Normal file
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* Handles underlying communication between the embedding window and the
|
||||
* editor frame.
|
||||
* @module EmbeddedSVGEdit
|
||||
*/
|
||||
|
||||
let cbid = 0;
|
||||
|
||||
/**
|
||||
* @callback module:EmbeddedSVGEdit.CallbackSetter
|
||||
* @param {GenericCallback} newCallback Callback to be stored (signature dependent on function)
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @callback module:EmbeddedSVGEdit.CallbackSetGetter
|
||||
* @param {...any} args Signature dependent on the function
|
||||
* @returns {module:EmbeddedSVGEdit.CallbackSetter}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} funcName
|
||||
* @returns {module:EmbeddedSVGEdit.CallbackSetGetter}
|
||||
*/
|
||||
function getCallbackSetter (funcName) {
|
||||
return function (...args) {
|
||||
const that = this, // New callback
|
||||
callbackID = this.send(funcName, args, function () { /* */ }); // The callback (currently it's nothing, but will be set later)
|
||||
|
||||
return function (newCallback) {
|
||||
that.callbacks[callbackID] = newCallback; // Set callback
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Having this separate from messageListener allows us to
|
||||
* avoid using JSON parsing (and its limitations) in the case
|
||||
* of same domain control.
|
||||
* @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value
|
||||
* @param {PlainObject} data
|
||||
* @param {JSON} data.result
|
||||
* @param {string} data.error
|
||||
* @param {Integer} data.id
|
||||
* @returns {void}
|
||||
*/
|
||||
function addCallback (t, {result, error, id: callbackID}) {
|
||||
if (typeof callbackID === 'number' && t.callbacks[callbackID]) {
|
||||
// These should be safe both because we check `cbid` is numeric and
|
||||
// because the calls are from trusted origins
|
||||
if (result) {
|
||||
t.callbacks[callbackID](result); // lgtm [js/unvalidated-dynamic-method-call]
|
||||
} else {
|
||||
t.callbacks[callbackID](error, 'error'); // lgtm [js/unvalidated-dynamic-method-call]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} e
|
||||
* @returns {void}
|
||||
*/
|
||||
function messageListener (e) {
|
||||
// We accept and post strings as opposed to objects for the sake of IE9 support; this
|
||||
// will most likely be changed in the future
|
||||
if (!e.data || !['string', 'object'].includes(typeof e.data)) {
|
||||
return;
|
||||
}
|
||||
const {allowedOrigins} = this,
|
||||
data = typeof e.data === 'object' ? e.data : JSON.parse(e.data);
|
||||
if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit' ||
|
||||
e.source !== this.frame.contentWindow ||
|
||||
(!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin))
|
||||
) {
|
||||
console.log(`The origin ${e.origin} was not whitelisted as an origin from which responses may be received by this ${window.origin} script.`); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
addCallback(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback module:EmbeddedSVGEdit.MessageListener
|
||||
* @param {MessageEvent} e
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value
|
||||
* @returns {module:EmbeddedSVGEdit.MessageListener} Event listener
|
||||
*/
|
||||
function getMessageListener (t) {
|
||||
return function (e) {
|
||||
messageListener.call(t, e);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedded SVG-edit API.
|
||||
* General usage:
|
||||
* - Have an iframe somewhere pointing to a version of svg-edit > r1000.
|
||||
* @example
|
||||
// Initialize the magic with:
|
||||
const svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit);
|
||||
|
||||
// Pass functions in this format:
|
||||
svgCanvas.setSvgString('string');
|
||||
|
||||
// Or if a callback is needed:
|
||||
svgCanvas.setSvgString('string')(function (data, error) {
|
||||
if (error) {
|
||||
// There was an error
|
||||
} else {
|
||||
// Handle data
|
||||
}
|
||||
});
|
||||
|
||||
// Everything is done with the same API as the real svg-edit,
|
||||
// and all documentation is unchanged.
|
||||
|
||||
// However, this file depends on the postMessage API which
|
||||
// can only support JSON-serializable arguments and
|
||||
// return values, so, for example, arguments whose value is
|
||||
// 'undefined', a function, a non-finite number, or a built-in
|
||||
// object like Date(), RegExp(), etc. will most likely not behave
|
||||
// as expected. In such a case one may need to host
|
||||
// the SVG editor on the same domain and reference the
|
||||
// JavaScript methods on the frame itself.
|
||||
|
||||
// The only other difference is when handling returns:
|
||||
// the callback notation is used instead.
|
||||
const blah = new EmbeddedSVGEdit(window.frames.svgedit);
|
||||
blah.clearSelection('woot', 'blah', 1337, [1, 2, 3, 4, 5, 'moo'], -42, {
|
||||
a: 'tree', b: 6, c: 9
|
||||
})(function () { console.log('GET DATA', args); });
|
||||
*
|
||||
* @memberof module:EmbeddedSVGEdit
|
||||
*/
|
||||
class EmbeddedSVGEdit {
|
||||
/**
|
||||
* @param {HTMLIFrameElement} frame
|
||||
* @param {string[]} [allowedOrigins=[]] Array of origins from which incoming
|
||||
* messages will be allowed when same origin is not used; defaults to none.
|
||||
* If supplied, it should probably be the same as svgEditor's allowedOrigins
|
||||
*/
|
||||
constructor (frame, allowedOrigins) {
|
||||
const that = this;
|
||||
this.allowedOrigins = allowedOrigins || [];
|
||||
// Initialize communication
|
||||
this.frame = frame;
|
||||
this.callbacks = {};
|
||||
// List of functions extracted with this:
|
||||
// Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html
|
||||
|
||||
// for (const i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q
|
||||
// const functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString',
|
||||
// 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility',
|
||||
// 'moveSelectedToLayer', 'clear'];
|
||||
|
||||
// Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API
|
||||
// const {svgCanvas} = frame.contentWindow;
|
||||
// const l = [];
|
||||
// for (const i in svgCanvas) { if (typeof svgCanvas[i] === 'function') { l.push(i);} };
|
||||
// alert("['" + l.join("', '") + "']");
|
||||
// Run in svgedit itself
|
||||
const functions = [
|
||||
'addExtension',
|
||||
'addSVGElementFromJson',
|
||||
'addToSelection',
|
||||
'alignSelectedElements',
|
||||
'assignAttributes',
|
||||
'bind',
|
||||
'call',
|
||||
'changeSelectedAttribute',
|
||||
'cleanupElement',
|
||||
'clear',
|
||||
'clearSelection',
|
||||
'clearSvgContentElement',
|
||||
'cloneLayer',
|
||||
'cloneSelectedElements',
|
||||
'convertGradients',
|
||||
'convertToGroup',
|
||||
'convertToNum',
|
||||
'convertToPath',
|
||||
'copySelectedElements',
|
||||
'createLayer',
|
||||
'cutSelectedElements',
|
||||
'cycleElement',
|
||||
'deleteCurrentLayer',
|
||||
'deleteSelectedElements',
|
||||
'embedImage',
|
||||
'exportPDF',
|
||||
'findDefs',
|
||||
'getBBox',
|
||||
'getBlur',
|
||||
'getBold',
|
||||
'getColor',
|
||||
'getContentElem',
|
||||
'getCurrentDrawing',
|
||||
'getDocumentTitle',
|
||||
'getEditorNS',
|
||||
'getElem',
|
||||
'getFillOpacity',
|
||||
'getFontColor',
|
||||
'getFontFamily',
|
||||
'getFontSize',
|
||||
'getHref',
|
||||
'getId',
|
||||
'getIntersectionList',
|
||||
'getItalic',
|
||||
'getMode',
|
||||
'getMouseTarget',
|
||||
'getNextId',
|
||||
'getOffset',
|
||||
'getOpacity',
|
||||
'getPaintOpacity',
|
||||
'getPrivateMethods',
|
||||
'getRefElem',
|
||||
'getResolution',
|
||||
'getRootElem',
|
||||
'getRotationAngle',
|
||||
'getSelectedElems',
|
||||
'getStrokeOpacity',
|
||||
'getStrokeWidth',
|
||||
'getStrokedBBox',
|
||||
'getStyle',
|
||||
'getSvgString',
|
||||
'getText',
|
||||
'getTitle',
|
||||
'getTransformList',
|
||||
'getUIStrings',
|
||||
'getUrlFromAttr',
|
||||
'getVersion',
|
||||
'getVisibleElements',
|
||||
'getVisibleElementsAndBBoxes',
|
||||
'getZoom',
|
||||
'groupSelectedElements',
|
||||
'groupSvgElem',
|
||||
'hasMatrixTransform',
|
||||
'identifyLayers',
|
||||
'importSvgString',
|
||||
'leaveContext',
|
||||
'linkControlPoints',
|
||||
'makeHyperlink',
|
||||
'matrixMultiply',
|
||||
'mergeAllLayers',
|
||||
'mergeLayer',
|
||||
'moveSelectedElements',
|
||||
'moveSelectedToLayer',
|
||||
'moveToBottomSelectedElement',
|
||||
'moveToTopSelectedElement',
|
||||
'moveUpDownSelected',
|
||||
'open',
|
||||
'pasteElements',
|
||||
'prepareSvg',
|
||||
'pushGroupProperties',
|
||||
'randomizeIds',
|
||||
'rasterExport',
|
||||
'ready',
|
||||
'recalculateAllSelectedDimensions',
|
||||
'recalculateDimensions',
|
||||
'remapElement',
|
||||
'removeFromSelection',
|
||||
'removeHyperlink',
|
||||
'removeUnusedDefElems',
|
||||
'renameCurrentLayer',
|
||||
'round',
|
||||
'runExtensions',
|
||||
'sanitizeSvg',
|
||||
'save',
|
||||
'selectAllInCurrentLayer',
|
||||
'selectOnly',
|
||||
'setBBoxZoom',
|
||||
'setBackground',
|
||||
'setBlur',
|
||||
'setBlurNoUndo',
|
||||
'setBlurOffsets',
|
||||
'setBold',
|
||||
'setColor',
|
||||
'setConfig',
|
||||
'setContext',
|
||||
'setCurrentLayer',
|
||||
'setCurrentLayerPosition',
|
||||
'setDocumentTitle',
|
||||
'setFillPaint',
|
||||
'setFontColor',
|
||||
'setFontFamily',
|
||||
'setFontSize',
|
||||
'setGoodImage',
|
||||
'setGradient',
|
||||
'setGroupTitle',
|
||||
'setHref',
|
||||
'setIdPrefix',
|
||||
'setImageURL',
|
||||
'setItalic',
|
||||
'setLayerVisibility',
|
||||
'setLinkURL',
|
||||
'setMode',
|
||||
'setOpacity',
|
||||
'setPaint',
|
||||
'setPaintOpacity',
|
||||
'setRectRadius',
|
||||
'setResolution',
|
||||
'setRotationAngle',
|
||||
'setSegType',
|
||||
'setStrokeAttr',
|
||||
'setStrokePaint',
|
||||
'setStrokeWidth',
|
||||
'setSvgString',
|
||||
'setTextContent',
|
||||
'setUiStrings',
|
||||
'setUseData',
|
||||
'setZoom',
|
||||
'svgCanvasToString',
|
||||
'svgToString',
|
||||
'transformListToTransform',
|
||||
'ungroupSelectedElement',
|
||||
'uniquifyElems',
|
||||
'updateCanvas',
|
||||
'zoomChanged'
|
||||
];
|
||||
|
||||
// TODO: rewrite the following, it's pretty scary.
|
||||
for (const func of functions) {
|
||||
this[func] = getCallbackSetter(func);
|
||||
}
|
||||
|
||||
// Older IE may need a polyfill for addEventListener, but so it would for SVG
|
||||
window.addEventListener('message', getMessageListener(this));
|
||||
window.addEventListener('keydown', (e) => {
|
||||
const {type, key} = e;
|
||||
if (key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
const keyboardEvent = new KeyboardEvent(type, {key});
|
||||
that.frame.contentDocument.dispatchEvent(keyboardEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {ArgumentsArray} args Signature dependent on function
|
||||
* @param {GenericCallback} callback (This may be better than a promise in case adding an event.)
|
||||
* @returns {Integer}
|
||||
*/
|
||||
send (name, args, callback) { // eslint-disable-line promise/prefer-await-to-callbacks
|
||||
const that = this;
|
||||
cbid++;
|
||||
|
||||
this.callbacks[cbid] = callback;
|
||||
setTimeout((function (callbackID) {
|
||||
return function () { // Delay for the callback to be set in case its synchronous
|
||||
/*
|
||||
* Todo: Handle non-JSON arguments and return values (undefined,
|
||||
* nonfinite numbers, functions, and built-in objects like Date,
|
||||
* RegExp), etc.? Allow promises instead of callbacks? Review
|
||||
* SVG-Edit functions for whether JSON-able parameters can be
|
||||
* made compatile with all API functionality
|
||||
*/
|
||||
// We accept and post strings for the sake of IE9 support
|
||||
let sameOriginWithGlobal = false;
|
||||
try {
|
||||
sameOriginWithGlobal = window.location.origin === that.frame.contentWindow.location.origin &&
|
||||
that.frame.contentWindow.svgEditor.canvas;
|
||||
} catch (err) {}
|
||||
|
||||
if (sameOriginWithGlobal) {
|
||||
// Although we do not really need this API if we are working same
|
||||
// domain, it could allow us to write in a way that would work
|
||||
// cross-domain as well, assuming we stick to the argument limitations
|
||||
// of the current JSON-based communication API (e.g., not passing
|
||||
// callbacks). We might be able to address these shortcomings; see
|
||||
// the todo elsewhere in this file.
|
||||
const message = {id: callbackID},
|
||||
{svgEditor: {canvas: svgCanvas}} = that.frame.contentWindow;
|
||||
try {
|
||||
message.result = svgCanvas[name](...args);
|
||||
} catch (err) {
|
||||
message.error = err.message;
|
||||
}
|
||||
addCallback(that, message);
|
||||
} else { // Requires the ext-xdomain-messaging.js extension
|
||||
that.frame.contentWindow.postMessage(JSON.stringify({
|
||||
namespace: 'svgCanvas', id: callbackID, name, args
|
||||
}), '*');
|
||||
}
|
||||
};
|
||||
}(cbid)), 0);
|
||||
|
||||
return cbid;
|
||||
}
|
||||
}
|
||||
|
||||
export default EmbeddedSVGEdit;
|
||||
12
src/editor/extensions/allowedMimeTypes.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
$allowedMimeTypesBySuffix = array(
|
||||
'svg' => 'image/svg+xml;charset=UTF-8',
|
||||
'png' => 'image/png',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'bmp' => 'image/bmp',
|
||||
'webp' => 'image/webp',
|
||||
'pdf' => 'application/pdf'
|
||||
);
|
||||
|
||||
?>
|
||||
BIN
src/editor/extensions/closepath.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
41
src/editor/extensions/closepath_icons.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="tool_closepath">
|
||||
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path stroke="#000" stroke-width="15" fill="#ffc8c8" d="m121.5,40l-84,106l27,115l166,2l29,-111"/>
|
||||
<line x1="240" y1="136" x2="169.5" y2="74" stroke="#A00" stroke-width="25" fill="none"/>
|
||||
<path stroke="none" fill ="#A00" d="m158,65l31,74l-3,-50l51,-3z"/>
|
||||
<g stroke-width="15" stroke="#00f" fill="#0ff">
|
||||
<circle r="30" cy="41" cx="123"/>
|
||||
<circle r="30" cy="146" cx="40"/>
|
||||
<circle r="30" cy="260" cx="69"/>
|
||||
<circle r="30" cy="260" cx="228"/>
|
||||
<circle r="30" cy="148" cx="260"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
<g id="tool_openpath">
|
||||
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<path stroke="#000" stroke-width="15" fill="#ffc8c8" d="m123.5,38l-84,106l27,115l166,2l29,-111"/>
|
||||
<line x1="276.5" y1="153" x2="108.5" y2="24" stroke="#000" stroke-width="10" fill="none"/>
|
||||
<g stroke-width="15" stroke="#00f" fill="#0ff">
|
||||
<circle r="30" cy="41" cx="123"/>
|
||||
<circle r="30" cy="146" cx="40"/>
|
||||
<circle r="30" cy="260" cx="69"/>
|
||||
<circle r="30" cy="260" cx="228"/>
|
||||
<circle r="30" cy="148" cx="260"/>
|
||||
</g>
|
||||
<g stroke="#A00" stroke-width="15" fill="none">
|
||||
<line x1="168" y1="24" x2="210" y2="150"/>
|
||||
<line x1="210" y1="24" x2="168" y2="150"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
|
||||
<g id="svg_eof"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
325
src/editor/extensions/ext-arrows.js
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* @file ext-arrows.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'arrows',
|
||||
async init (S) {
|
||||
const strings = await S.importLocale();
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const // {svgcontent} = S,
|
||||
addElem = svgCanvas.addSVGElementFromJson,
|
||||
{nonce, $} = S,
|
||||
prefix = 'se_arrow_';
|
||||
|
||||
let selElems, arrowprefix, randomizeIds = S.randomize_ids;
|
||||
|
||||
/**
|
||||
* @param {Window} win
|
||||
* @param {!(string|Integer)} n
|
||||
* @returns {void}
|
||||
*/
|
||||
function setArrowNonce (win, n) {
|
||||
randomizeIds = true;
|
||||
arrowprefix = prefix + n + '_';
|
||||
pathdata.fw.id = arrowprefix + 'fw';
|
||||
pathdata.bk.id = arrowprefix + 'bk';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Window} win
|
||||
* @returns {void}
|
||||
*/
|
||||
function unsetArrowNonce (win) {
|
||||
randomizeIds = false;
|
||||
arrowprefix = prefix;
|
||||
pathdata.fw.id = arrowprefix + 'fw';
|
||||
pathdata.bk.id = arrowprefix + 'bk';
|
||||
}
|
||||
|
||||
svgCanvas.bind('setnonce', setArrowNonce);
|
||||
svgCanvas.bind('unsetnonce', unsetArrowNonce);
|
||||
|
||||
if (randomizeIds) {
|
||||
arrowprefix = prefix + nonce + '_';
|
||||
} else {
|
||||
arrowprefix = prefix;
|
||||
}
|
||||
|
||||
const pathdata = {
|
||||
fw: {d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw'},
|
||||
bk: {d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', refx: 2, id: arrowprefix + 'bk'}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets linked element.
|
||||
* @param {Element} elem
|
||||
* @param {string} attr
|
||||
* @returns {Element}
|
||||
*/
|
||||
function getLinked (elem, attr) {
|
||||
const str = elem.getAttribute(attr);
|
||||
if (!str) { return null; }
|
||||
const m = str.match(/\(#(.*)\)/);
|
||||
// const m = str.match(/\(#(?<id>.+)\)/);
|
||||
// if (!m || !m.groups.id) {
|
||||
if (!m || m.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
return svgCanvas.getElem(m[1]);
|
||||
// return svgCanvas.getElem(m.groups.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function showPanel (on) {
|
||||
$('#arrow_panel').toggle(on);
|
||||
if (on) {
|
||||
const el = selElems[0];
|
||||
const end = el.getAttribute('marker-end');
|
||||
const start = el.getAttribute('marker-start');
|
||||
const mid = el.getAttribute('marker-mid');
|
||||
let val;
|
||||
if (end && start) {
|
||||
val = 'both';
|
||||
} else if (end) {
|
||||
val = 'end';
|
||||
} else if (start) {
|
||||
val = 'start';
|
||||
} else if (mid) {
|
||||
val = 'mid';
|
||||
if (mid.includes('bk')) {
|
||||
val = 'mid_bk';
|
||||
}
|
||||
}
|
||||
|
||||
if (!start && !mid && !end) {
|
||||
val = 'none';
|
||||
}
|
||||
|
||||
$('#arrow_list').val(val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function resetMarker () {
|
||||
const el = selElems[0];
|
||||
el.removeAttribute('marker-start');
|
||||
el.removeAttribute('marker-mid');
|
||||
el.removeAttribute('marker-end');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"bk"|"fw"} dir
|
||||
* @param {"both"|"mid"|"end"|"start"} type
|
||||
* @param {string} id
|
||||
* @returns {Element}
|
||||
*/
|
||||
function addMarker (dir, type, id) {
|
||||
// TODO: Make marker (or use?) per arrow type, since refX can be different
|
||||
id = id || arrowprefix + dir;
|
||||
|
||||
const data = pathdata[dir];
|
||||
|
||||
if (type === 'mid') {
|
||||
data.refx = 5;
|
||||
}
|
||||
|
||||
let marker = svgCanvas.getElem(id);
|
||||
if (!marker) {
|
||||
marker = addElem({
|
||||
element: 'marker',
|
||||
attr: {
|
||||
viewBox: '0 0 10 10',
|
||||
id,
|
||||
refY: 5,
|
||||
markerUnits: 'strokeWidth',
|
||||
markerWidth: 5,
|
||||
markerHeight: 5,
|
||||
orient: 'auto',
|
||||
style: 'pointer-events:none' // Currently needed for Opera
|
||||
}
|
||||
});
|
||||
const arrow = addElem({
|
||||
element: 'path',
|
||||
attr: {
|
||||
d: data.d,
|
||||
fill: '#000000'
|
||||
}
|
||||
});
|
||||
marker.append(arrow);
|
||||
svgCanvas.findDefs().append(marker);
|
||||
}
|
||||
|
||||
marker.setAttribute('refX', data.refx);
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function setArrow () {
|
||||
resetMarker();
|
||||
|
||||
let type = this.value;
|
||||
if (type === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set marker on element
|
||||
let dir = 'fw';
|
||||
if (type === 'mid_bk') {
|
||||
type = 'mid';
|
||||
dir = 'bk';
|
||||
} else if (type === 'both') {
|
||||
addMarker('bk', type);
|
||||
svgCanvas.changeSelectedAttribute('marker-start', 'url(#' + pathdata.bk.id + ')');
|
||||
type = 'end';
|
||||
dir = 'fw';
|
||||
} else if (type === 'start') {
|
||||
dir = 'bk';
|
||||
}
|
||||
|
||||
addMarker(dir, type);
|
||||
svgCanvas.changeSelectedAttribute('marker-' + type, 'url(#' + pathdata[dir].id + ')');
|
||||
svgCanvas.call('changed', selElems);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} elem
|
||||
* @returns {void}
|
||||
*/
|
||||
function colorChanged (elem) {
|
||||
const color = elem.getAttribute('stroke');
|
||||
const mtypes = ['start', 'mid', 'end'];
|
||||
const defs = svgCanvas.findDefs();
|
||||
|
||||
$.each(mtypes, function (i, type) {
|
||||
const marker = getLinked(elem, 'marker-' + type);
|
||||
if (!marker) { return; }
|
||||
|
||||
const curColor = $(marker).children().attr('fill');
|
||||
const curD = $(marker).children().attr('d');
|
||||
if (curColor === color) { return; }
|
||||
|
||||
const allMarkers = $(defs).find('marker');
|
||||
let newMarker = null;
|
||||
// Different color, check if already made
|
||||
allMarkers.each(function () {
|
||||
const attrs = $(this).children().attr(['fill', 'd']);
|
||||
if (attrs.fill === color && attrs.d === curD) {
|
||||
// Found another marker with this color and this path
|
||||
newMarker = this; // eslint-disable-line consistent-this
|
||||
}
|
||||
});
|
||||
|
||||
if (!newMarker) {
|
||||
// Create a new marker with this color
|
||||
const lastId = marker.id;
|
||||
const dir = lastId.includes('_fw') ? 'fw' : 'bk';
|
||||
|
||||
newMarker = addMarker(dir, type, arrowprefix + dir + allMarkers.length);
|
||||
|
||||
$(newMarker).children().attr('fill', color);
|
||||
}
|
||||
|
||||
$(elem).attr('marker-' + type, 'url(#' + newMarker.id + ')');
|
||||
|
||||
// Check if last marker can be removed
|
||||
let remove = true;
|
||||
$(S.svgcontent).find('line, polyline, path, polygon').each(function () {
|
||||
const element = this; // eslint-disable-line consistent-this
|
||||
$.each(mtypes, function (j, mtype) {
|
||||
if ($(element).attr('marker-' + mtype) === 'url(#' + marker.id + ')') {
|
||||
remove = false;
|
||||
return remove;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
if (!remove) { return false; }
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Not found, so can safely remove
|
||||
if (remove) {
|
||||
$(marker).remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const contextTools = [
|
||||
{
|
||||
type: 'select',
|
||||
panel: 'arrow_panel',
|
||||
id: 'arrow_list',
|
||||
defval: 'none',
|
||||
events: {
|
||||
change: setArrow
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
context_tools: strings.contextTools.map((contextTool, i) => {
|
||||
return Object.assign(contextTools[i], contextTool);
|
||||
}),
|
||||
callback () {
|
||||
$('#arrow_panel').hide();
|
||||
// Set ID so it can be translated in locale file
|
||||
$('#arrow_list option')[0].id = 'connector_no_arrow';
|
||||
},
|
||||
async addLangData ({lang, importLocale}) {
|
||||
const {langList} = await importLocale();
|
||||
return {
|
||||
data: langList
|
||||
};
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
// Use this to update the current selected elements
|
||||
selElems = opts.elems;
|
||||
|
||||
const markerElems = ['line', 'path', 'polyline', 'polygon'];
|
||||
let i = selElems.length;
|
||||
while (i--) {
|
||||
const elem = selElems[i];
|
||||
if (elem && markerElems.includes(elem.tagName)) {
|
||||
if (opts.selectedElement && !opts.multiselected) {
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
elementChanged (opts) {
|
||||
const elem = opts.elems[0];
|
||||
if (elem && (
|
||||
elem.getAttribute('marker-start') ||
|
||||
elem.getAttribute('marker-mid') ||
|
||||
elem.getAttribute('marker-end')
|
||||
)) {
|
||||
// const start = elem.getAttribute('marker-start');
|
||||
// const mid = elem.getAttribute('marker-mid');
|
||||
// const end = elem.getAttribute('marker-end');
|
||||
// Has marker, so see if it should match color
|
||||
colorChanged(elem);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
101
src/editor/extensions/ext-closepath.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @file ext-closepath.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Jeff Schiller
|
||||
*
|
||||
*/
|
||||
import '../svgpathseg.js';
|
||||
|
||||
// This extension adds a simple button to the contextual panel for paths
|
||||
// The button toggles whether the path is open or closed
|
||||
export default {
|
||||
name: 'closepath',
|
||||
async init ({importLocale, $}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
let selElems;
|
||||
const updateButton = function (path) {
|
||||
const seglist = path.pathSegList,
|
||||
closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType === 1,
|
||||
showbutton = closed ? '#tool_openpath' : '#tool_closepath',
|
||||
hidebutton = closed ? '#tool_closepath' : '#tool_openpath';
|
||||
$(hidebutton).hide();
|
||||
$(showbutton).show();
|
||||
};
|
||||
const showPanel = function (on) {
|
||||
$('#closepath_panel').toggle(on);
|
||||
if (on) {
|
||||
const path = selElems[0];
|
||||
if (path) { updateButton(path); }
|
||||
}
|
||||
};
|
||||
const toggleClosed = function () {
|
||||
const path = selElems[0];
|
||||
if (path) {
|
||||
const seglist = path.pathSegList,
|
||||
last = seglist.numberOfItems - 1;
|
||||
// is closed
|
||||
if (seglist.getItem(last).pathSegType === 1) {
|
||||
seglist.removeItem(last);
|
||||
} else {
|
||||
seglist.appendItem(path.createSVGPathSegClosePath());
|
||||
}
|
||||
updateButton(path);
|
||||
}
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: 'tool_openpath',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'openpath.png',
|
||||
type: 'context',
|
||||
panel: 'closepath_panel',
|
||||
events: {
|
||||
click () {
|
||||
toggleClosed();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'tool_closepath',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'closepath.png',
|
||||
type: 'context',
|
||||
panel: 'closepath_panel',
|
||||
events: {
|
||||
click () {
|
||||
toggleClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'closepath_icons.svg',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
callback () {
|
||||
$('#closepath_panel').hide();
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
selElems = opts.elems;
|
||||
let i = selElems.length;
|
||||
while (i--) {
|
||||
const elem = selElems[i];
|
||||
if (elem && elem.tagName === 'path') {
|
||||
if (opts.selectedElement && !opts.multiselected) {
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
660
src/editor/extensions/ext-connector.js
Normal file
@@ -0,0 +1,660 @@
|
||||
/* eslint-disable unicorn/no-fn-reference-in-iterator */
|
||||
/**
|
||||
* @file ext-connector.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'connector',
|
||||
async init (S) {
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const {getElem} = svgCanvas;
|
||||
const {$, svgroot, importLocale} = S,
|
||||
addElem = svgCanvas.addSVGElementFromJson,
|
||||
selManager = S.selectorManager,
|
||||
connSel = '.se_connector',
|
||||
// connect_str = '-SE_CONNECT-',
|
||||
elData = $.data;
|
||||
const strings = await importLocale();
|
||||
|
||||
let startX,
|
||||
startY,
|
||||
curLine,
|
||||
startElem,
|
||||
endElem,
|
||||
seNs,
|
||||
{svgcontent} = S,
|
||||
started = false,
|
||||
connections = [],
|
||||
selElems = [];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Float} x
|
||||
* @param {Float} y
|
||||
* @param {module:utilities.BBoxObject} bb
|
||||
* @param {Float} offset
|
||||
* @returns {module:math.XYObject}
|
||||
*/
|
||||
function getBBintersect (x, y, bb, offset) {
|
||||
if (offset) {
|
||||
offset -= 0;
|
||||
bb = $.extend({}, bb);
|
||||
bb.width += offset;
|
||||
bb.height += offset;
|
||||
bb.x -= offset / 2;
|
||||
bb.y -= offset / 2;
|
||||
}
|
||||
|
||||
const midX = bb.x + bb.width / 2;
|
||||
const midY = bb.y + bb.height / 2;
|
||||
const lenX = x - midX;
|
||||
const lenY = y - midY;
|
||||
|
||||
const slope = Math.abs(lenY / lenX);
|
||||
|
||||
let ratio;
|
||||
if (slope < bb.height / bb.width) {
|
||||
ratio = (bb.width / 2) / Math.abs(lenX);
|
||||
} else {
|
||||
ratio = lenY
|
||||
? (bb.height / 2) / Math.abs(lenY)
|
||||
: 0;
|
||||
}
|
||||
|
||||
return {
|
||||
x: midX + lenX * ratio,
|
||||
y: midY + lenY * ratio
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"start"|"end"} side
|
||||
* @param {Element} line
|
||||
* @returns {Float}
|
||||
*/
|
||||
function getOffset (side, line) {
|
||||
const giveOffset = line.getAttribute('marker-' + side);
|
||||
// const giveOffset = $(line).data(side+'_off');
|
||||
|
||||
// TODO: Make this number (5) be based on marker width/height
|
||||
const size = line.getAttribute('stroke-width') * 5;
|
||||
return giveOffset ? size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function showPanel (on) {
|
||||
let connRules = $('#connector_rules');
|
||||
if (!connRules.length) {
|
||||
connRules = $('<style id="connector_rules"></style>').appendTo('head');
|
||||
}
|
||||
connRules.text(!on ? '' : '#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }');
|
||||
$('#connector_panel').toggle(on);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} elem
|
||||
* @param {Integer|"end"} pos
|
||||
* @param {Float} x
|
||||
* @param {Float} y
|
||||
* @param {boolean} [setMid]
|
||||
* @returns {void}
|
||||
*/
|
||||
function setPoint (elem, pos, x, y, setMid) {
|
||||
const pts = elem.points;
|
||||
const pt = svgroot.createSVGPoint();
|
||||
pt.x = x;
|
||||
pt.y = y;
|
||||
if (pos === 'end') { pos = pts.numberOfItems - 1; }
|
||||
// TODO: Test for this on init, then use alt only if needed
|
||||
try {
|
||||
pts.replaceItem(pt, pos);
|
||||
} catch (err) {
|
||||
// Should only occur in FF which formats points attr as "n,n n,n", so just split
|
||||
const ptArr = elem.getAttribute('points').split(' ');
|
||||
for (let i = 0; i < ptArr.length; i++) {
|
||||
if (i === pos) {
|
||||
ptArr[i] = x + ',' + y;
|
||||
}
|
||||
}
|
||||
elem.setAttribute('points', ptArr.join(' '));
|
||||
}
|
||||
|
||||
if (setMid) {
|
||||
// Add center point
|
||||
const ptStart = pts.getItem(0);
|
||||
const ptEnd = pts.getItem(pts.numberOfItems - 1);
|
||||
setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Float} diffX
|
||||
* @param {Float} diffY
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateLine (diffX, diffY) {
|
||||
// Update line with element
|
||||
let i = connections.length;
|
||||
while (i--) {
|
||||
const conn = connections[i];
|
||||
const line = conn.connector;
|
||||
// const {elem} = conn;
|
||||
|
||||
const pre = conn.is_start ? 'start' : 'end';
|
||||
// const sw = line.getAttribute('stroke-width') * 5;
|
||||
|
||||
// Update bbox for this element
|
||||
const bb = elData(line, pre + '_bb');
|
||||
bb.x = conn.start_x + diffX;
|
||||
bb.y = conn.start_y + diffY;
|
||||
elData(line, pre + '_bb', bb);
|
||||
|
||||
const altPre = conn.is_start ? 'end' : 'start';
|
||||
|
||||
// Get center pt of connected element
|
||||
const bb2 = elData(line, altPre + '_bb');
|
||||
const srcX = bb2.x + bb2.width / 2;
|
||||
const srcY = bb2.y + bb2.height / 2;
|
||||
|
||||
// Set point of element being moved
|
||||
const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0
|
||||
setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true);
|
||||
|
||||
// Set point of connected element
|
||||
const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line));
|
||||
setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element[]} [elems=selElems] Array of elements
|
||||
* @returns {void}
|
||||
*/
|
||||
function findConnectors (elems = selElems) {
|
||||
const connectors = $(svgcontent).find(connSel);
|
||||
connections = [];
|
||||
|
||||
// Loop through connectors to see if one is connected to the element
|
||||
connectors.each(function () {
|
||||
let addThis;
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function add () {
|
||||
if (elems.includes(this)) {
|
||||
// Pretend this element is selected
|
||||
addThis = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the ends
|
||||
const parts = [];
|
||||
['start', 'end'].forEach(function (pos, i) {
|
||||
const key = 'c_' + pos;
|
||||
let part = elData(this, key);
|
||||
if (part === null || part === undefined) { // Does this ever return nullish values?
|
||||
part = document.getElementById(
|
||||
this.attributes['se:connector'].value.split(' ')[i]
|
||||
);
|
||||
elData(this, 'c_' + pos, part.id);
|
||||
elData(this, pos + '_bb', svgCanvas.getStrokedBBox([part]));
|
||||
} else part = document.getElementById(part);
|
||||
parts.push(part);
|
||||
}, this);
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const cElem = parts[i];
|
||||
|
||||
addThis = false;
|
||||
// The connected element might be part of a selected group
|
||||
$(cElem).parents().each(add);
|
||||
|
||||
if (!cElem || !cElem.parentNode) {
|
||||
$(this).remove();
|
||||
continue;
|
||||
}
|
||||
if (elems.includes(cElem) || addThis) {
|
||||
const bb = svgCanvas.getStrokedBBox([cElem]);
|
||||
connections.push({
|
||||
elem: cElem,
|
||||
connector: this,
|
||||
is_start: (i === 0),
|
||||
start_x: bb.x,
|
||||
start_y: bb.y
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element[]} [elems=selElems]
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateConnectors (elems) {
|
||||
// Updates connector lines based on selected elements
|
||||
// Is not used on mousemove, as it runs getStrokedBBox every time,
|
||||
// which isn't necessary there.
|
||||
findConnectors(elems);
|
||||
if (connections.length) {
|
||||
// Update line with element
|
||||
let i = connections.length;
|
||||
while (i--) {
|
||||
const conn = connections[i];
|
||||
const line = conn.connector;
|
||||
const {elem} = conn;
|
||||
|
||||
// const sw = line.getAttribute('stroke-width') * 5;
|
||||
const pre = conn.is_start ? 'start' : 'end';
|
||||
|
||||
// Update bbox for this element
|
||||
const bb = svgCanvas.getStrokedBBox([elem]);
|
||||
bb.x = conn.start_x;
|
||||
bb.y = conn.start_y;
|
||||
elData(line, pre + '_bb', bb);
|
||||
/* const addOffset = */ elData(line, pre + '_off');
|
||||
|
||||
const altPre = conn.is_start ? 'end' : 'start';
|
||||
|
||||
// Get center pt of connected element
|
||||
const bb2 = elData(line, altPre + '_bb');
|
||||
const srcX = bb2.x + bb2.width / 2;
|
||||
const srcY = bb2.y + bb2.height / 2;
|
||||
|
||||
// Set point of element being moved
|
||||
let pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line));
|
||||
setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true);
|
||||
|
||||
// Set point of connected element
|
||||
const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line));
|
||||
setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true);
|
||||
|
||||
// Update points attribute manually for webkit
|
||||
if (navigator.userAgent.includes('AppleWebKit')) {
|
||||
const pts = line.points;
|
||||
const len = pts.numberOfItems;
|
||||
const ptArr = [];
|
||||
for (let j = 0; j < len; j++) {
|
||||
pt = pts.getItem(j);
|
||||
ptArr[j] = pt.x + ',' + pt.y;
|
||||
}
|
||||
line.setAttribute('points', ptArr.join(' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do once
|
||||
(function () {
|
||||
const gse = svgCanvas.groupSelectedElements;
|
||||
|
||||
svgCanvas.groupSelectedElements = function (...args) {
|
||||
svgCanvas.removeFromSelection($(connSel).toArray());
|
||||
return gse.apply(this, args);
|
||||
};
|
||||
|
||||
const mse = svgCanvas.moveSelectedElements;
|
||||
|
||||
svgCanvas.moveSelectedElements = function (...args) {
|
||||
const cmd = mse.apply(this, args);
|
||||
updateConnectors();
|
||||
return cmd;
|
||||
};
|
||||
|
||||
seNs = svgCanvas.getEditorNS();
|
||||
}());
|
||||
|
||||
/**
|
||||
* Do on reset.
|
||||
* @returns {void}
|
||||
*/
|
||||
function init () {
|
||||
// Make sure all connectors have data set
|
||||
$(svgcontent).find('*').each(function () {
|
||||
const conn = this.getAttributeNS(seNs, 'connector');
|
||||
if (conn) {
|
||||
this.setAttribute('class', connSel.substr(1));
|
||||
const connData = conn.split(' ');
|
||||
const sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]);
|
||||
const ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]);
|
||||
$(this).data('c_start', connData[0])
|
||||
.data('c_end', connData[1])
|
||||
.data('start_bb', sbb)
|
||||
.data('end_bb', ebb);
|
||||
svgCanvas.getEditorNS(true);
|
||||
}
|
||||
});
|
||||
// updateConnectors();
|
||||
}
|
||||
|
||||
// $(svgroot).parent().mousemove(function (e) {
|
||||
// // if (started
|
||||
// // || svgCanvas.getMode() !== 'connector'
|
||||
// // || e.target.parentNode.parentNode !== svgcontent) return;
|
||||
//
|
||||
// console.log('y')
|
||||
// // if (e.target.parentNode.parentNode === svgcontent) {
|
||||
// //
|
||||
// // }
|
||||
// });
|
||||
|
||||
const buttons = [{
|
||||
id: 'mode_connect',
|
||||
type: 'mode',
|
||||
icon: svgEditor.curConfig.imgPath + 'cut.png',
|
||||
includeWith: {
|
||||
button: '#tool_line',
|
||||
isDefault: false,
|
||||
position: 1
|
||||
},
|
||||
events: {
|
||||
click () {
|
||||
svgCanvas.setMode('connector');
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.imgPath + 'conn.svg',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
/* async */ addLangData ({lang}) { // , importLocale: importLoc
|
||||
return {
|
||||
data: strings.langList
|
||||
};
|
||||
},
|
||||
mouseDown (opts) {
|
||||
const e = opts.event;
|
||||
startX = opts.start_x;
|
||||
startY = opts.start_y;
|
||||
const mode = svgCanvas.getMode();
|
||||
const {curConfig: {initStroke}} = svgEditor;
|
||||
|
||||
if (mode === 'connector') {
|
||||
if (started) { return undefined; }
|
||||
|
||||
const mouseTarget = e.target;
|
||||
|
||||
const parents = $(mouseTarget).parents();
|
||||
|
||||
if ($.inArray(svgcontent, parents) !== -1) {
|
||||
// Connectable element
|
||||
|
||||
// If child of foreignObject, use parent
|
||||
const fo = $(mouseTarget).closest('foreignObject');
|
||||
startElem = fo.length ? fo[0] : mouseTarget;
|
||||
|
||||
// Get center of source element
|
||||
const bb = svgCanvas.getStrokedBBox([startElem]);
|
||||
const x = bb.x + bb.width / 2;
|
||||
const y = bb.y + bb.height / 2;
|
||||
|
||||
started = true;
|
||||
curLine = addElem({
|
||||
element: 'polyline',
|
||||
attr: {
|
||||
id: svgCanvas.getNextId(),
|
||||
points: (x + ',' + y + ' ' + x + ',' + y + ' ' + startX + ',' + startY),
|
||||
stroke: '#' + initStroke.color,
|
||||
'stroke-width': (!startElem.stroke_width || startElem.stroke_width === 0)
|
||||
? initStroke.width
|
||||
: startElem.stroke_width,
|
||||
fill: 'none',
|
||||
opacity: initStroke.opacity,
|
||||
style: 'pointer-events:none'
|
||||
}
|
||||
});
|
||||
elData(curLine, 'start_bb', bb);
|
||||
}
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
}
|
||||
if (mode === 'select') {
|
||||
findConnectors();
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
mouseMove (opts) {
|
||||
const zoom = svgCanvas.getZoom();
|
||||
// const e = opts.event;
|
||||
const x = opts.mouse_x / zoom;
|
||||
const y = opts.mouse_y / zoom;
|
||||
|
||||
const diffX = x - startX,
|
||||
diffY = y - startY;
|
||||
|
||||
const mode = svgCanvas.getMode();
|
||||
|
||||
if (mode === 'connector' && started) {
|
||||
// const sw = curLine.getAttribute('stroke-width') * 3;
|
||||
// Set start point (adjusts based on bb)
|
||||
const pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine));
|
||||
startX = pt.x;
|
||||
startY = pt.y;
|
||||
|
||||
setPoint(curLine, 0, pt.x, pt.y, true);
|
||||
|
||||
// Set end point
|
||||
setPoint(curLine, 'end', x, y, true);
|
||||
} else if (mode === 'select') {
|
||||
let slen = selElems.length;
|
||||
while (slen--) {
|
||||
const elem = selElems[slen];
|
||||
// Look for selected connector elements
|
||||
if (elem && elData(elem, 'c_start')) {
|
||||
// Remove the "translate" transform given to move
|
||||
svgCanvas.removeFromSelection([elem]);
|
||||
svgCanvas.getTransformList(elem).clear();
|
||||
}
|
||||
}
|
||||
if (connections.length) {
|
||||
updateLine(diffX, diffY);
|
||||
}
|
||||
}
|
||||
},
|
||||
mouseUp (opts) {
|
||||
// const zoom = svgCanvas.getZoom();
|
||||
const e = opts.event;
|
||||
// , x = opts.mouse_x / zoom,
|
||||
// , y = opts.mouse_y / zoom,
|
||||
let mouseTarget = e.target;
|
||||
|
||||
if (svgCanvas.getMode() !== 'connector') {
|
||||
return undefined;
|
||||
}
|
||||
const fo = $(mouseTarget).closest('foreignObject');
|
||||
if (fo.length) { mouseTarget = fo[0]; }
|
||||
|
||||
const parents = $(mouseTarget).parents();
|
||||
|
||||
if (mouseTarget === startElem) {
|
||||
// Start line through click
|
||||
started = true;
|
||||
return {
|
||||
keep: true,
|
||||
element: null,
|
||||
started
|
||||
};
|
||||
}
|
||||
if ($.inArray(svgcontent, parents) === -1) {
|
||||
// Not a valid target element, so remove line
|
||||
$(curLine).remove();
|
||||
started = false;
|
||||
return {
|
||||
keep: false,
|
||||
element: null,
|
||||
started
|
||||
};
|
||||
}
|
||||
// Valid end element
|
||||
endElem = mouseTarget;
|
||||
|
||||
const startId = startElem.id, endId = endElem.id;
|
||||
const connStr = startId + ' ' + endId;
|
||||
const altStr = endId + ' ' + startId;
|
||||
// Don't create connector if one already exists
|
||||
const dupe = $(svgcontent).find(connSel).filter(function () {
|
||||
const conn = this.getAttributeNS(seNs, 'connector');
|
||||
if (conn === connStr || conn === altStr) { return true; }
|
||||
return false;
|
||||
});
|
||||
if (dupe.length) {
|
||||
$(curLine).remove();
|
||||
return {
|
||||
keep: false,
|
||||
element: null,
|
||||
started: false
|
||||
};
|
||||
}
|
||||
|
||||
const bb = svgCanvas.getStrokedBBox([endElem]);
|
||||
|
||||
const pt = getBBintersect(startX, startY, bb, getOffset('start', curLine));
|
||||
setPoint(curLine, 'end', pt.x, pt.y, true);
|
||||
$(curLine)
|
||||
.data('c_start', startId)
|
||||
.data('c_end', endId)
|
||||
.data('end_bb', bb);
|
||||
seNs = svgCanvas.getEditorNS(true);
|
||||
curLine.setAttributeNS(seNs, 'se:connector', connStr);
|
||||
curLine.setAttribute('class', connSel.substr(1));
|
||||
curLine.setAttribute('opacity', 1);
|
||||
svgCanvas.addToSelection([curLine]);
|
||||
svgCanvas.moveToBottomSelectedElement();
|
||||
selManager.requestSelector(curLine).showGrips(false);
|
||||
started = false;
|
||||
return {
|
||||
keep: true,
|
||||
element: curLine,
|
||||
started
|
||||
};
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
// TODO: Find better way to skip operations if no connectors are in use
|
||||
if (!$(svgcontent).find(connSel).length) { return; }
|
||||
|
||||
if (svgCanvas.getMode() === 'connector') {
|
||||
svgCanvas.setMode('select');
|
||||
}
|
||||
|
||||
// Use this to update the current selected elements
|
||||
selElems = opts.elems;
|
||||
|
||||
let i = selElems.length;
|
||||
while (i--) {
|
||||
const elem = selElems[i];
|
||||
if (elem && elData(elem, 'c_start')) {
|
||||
selManager.requestSelector(elem).showGrips(false);
|
||||
if (opts.selectedElement && !opts.multiselected) {
|
||||
// TODO: Set up context tools and hide most regular line tools
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
}
|
||||
updateConnectors();
|
||||
},
|
||||
elementChanged (opts) {
|
||||
let elem = opts.elems[0];
|
||||
if (!elem) return;
|
||||
if (elem.tagName === 'svg' && elem.id === 'svgcontent') {
|
||||
// Update svgcontent (can change on import)
|
||||
svgcontent = elem;
|
||||
init();
|
||||
}
|
||||
|
||||
// Has marker, so change offset
|
||||
if (
|
||||
elem.getAttribute('marker-start') ||
|
||||
elem.getAttribute('marker-mid') ||
|
||||
elem.getAttribute('marker-end')
|
||||
) {
|
||||
const start = elem.getAttribute('marker-start');
|
||||
const mid = elem.getAttribute('marker-mid');
|
||||
const end = elem.getAttribute('marker-end');
|
||||
curLine = elem;
|
||||
$(elem)
|
||||
.data('start_off', Boolean(start))
|
||||
.data('end_off', Boolean(end));
|
||||
|
||||
if (elem.tagName === 'line' && mid) {
|
||||
// Convert to polyline to accept mid-arrow
|
||||
|
||||
const x1 = Number(elem.getAttribute('x1'));
|
||||
const x2 = Number(elem.getAttribute('x2'));
|
||||
const y1 = Number(elem.getAttribute('y1'));
|
||||
const y2 = Number(elem.getAttribute('y2'));
|
||||
const {id} = elem;
|
||||
|
||||
const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' ');
|
||||
const pline = addElem({
|
||||
element: 'polyline',
|
||||
attr: {
|
||||
points: (x1 + ',' + y1 + midPt + x2 + ',' + y2),
|
||||
stroke: elem.getAttribute('stroke'),
|
||||
'stroke-width': elem.getAttribute('stroke-width'),
|
||||
'marker-mid': mid,
|
||||
fill: 'none',
|
||||
opacity: elem.getAttribute('opacity') || 1
|
||||
}
|
||||
});
|
||||
$(elem).after(pline).remove();
|
||||
svgCanvas.clearSelection();
|
||||
pline.id = id;
|
||||
svgCanvas.addToSelection([pline]);
|
||||
elem = pline;
|
||||
}
|
||||
}
|
||||
// Update line if it's a connector
|
||||
if (elem.getAttribute('class') === connSel.substr(1)) {
|
||||
const start = getElem(elData(elem, 'c_start'));
|
||||
updateConnectors([start]);
|
||||
} else {
|
||||
updateConnectors();
|
||||
}
|
||||
},
|
||||
IDsUpdated (input) {
|
||||
const remove = [];
|
||||
input.elems.forEach(function (elem) {
|
||||
if ('se:connector' in elem.attr) {
|
||||
elem.attr['se:connector'] = elem.attr['se:connector'].split(' ')
|
||||
.map(function (oldID) { return input.changes[oldID]; }).join(' ');
|
||||
|
||||
// Check validity - the field would be something like 'svg_21 svg_22', but
|
||||
// if one end is missing, it would be 'svg_21' and therefore fail this test
|
||||
if (!(/. ./).test(elem.attr['se:connector'])) {
|
||||
remove.push(elem.attr.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
return {remove};
|
||||
},
|
||||
toolButtonStateUpdate (opts) {
|
||||
if (opts.nostroke) {
|
||||
if ($('#mode_connect').hasClass('tool_button_current')) {
|
||||
svgEditor.clickSelect();
|
||||
}
|
||||
}
|
||||
$('#mode_connect')
|
||||
.toggleClass('disabled', opts.nostroke);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
115
src/editor/extensions/ext-eyedropper.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @file ext-eyedropper.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Jeff Schiller
|
||||
*
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'eyedropper',
|
||||
async init (S) {
|
||||
const strings = await S.importLocale();
|
||||
const svgEditor = this;
|
||||
const {$, ChangeElementCommand} = S, // , svgcontent,
|
||||
// svgdoc = S.svgroot.parentNode.ownerDocument,
|
||||
svgCanvas = svgEditor.canvas,
|
||||
addToHistory = function (cmd) { svgCanvas.undoMgr.addCommandToHistory(cmd); },
|
||||
currentStyle = {
|
||||
fillPaint: 'red', fillOpacity: 1.0,
|
||||
strokePaint: 'black', strokeOpacity: 1.0,
|
||||
strokeWidth: 5, strokeDashArray: null,
|
||||
opacity: 1.0,
|
||||
strokeLinecap: 'butt',
|
||||
strokeLinejoin: 'miter'
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {module:svgcanvas.SvgCanvas#event:ext_selectedChanged|module:svgcanvas.SvgCanvas#event:ext_elementChanged} opts
|
||||
* @returns {void}
|
||||
*/
|
||||
function getStyle (opts) {
|
||||
// if we are in eyedropper mode, we don't want to disable the eye-dropper tool
|
||||
const mode = svgCanvas.getMode();
|
||||
if (mode === 'eyedropper') { return; }
|
||||
|
||||
const tool = $('#tool_eyedropper');
|
||||
// enable-eye-dropper if one element is selected
|
||||
let elem = null;
|
||||
if (!opts.multiselected && opts.elems[0] &&
|
||||
!['svg', 'g', 'use'].includes(opts.elems[0].nodeName)
|
||||
) {
|
||||
elem = opts.elems[0];
|
||||
tool.removeClass('disabled');
|
||||
// grab the current style
|
||||
currentStyle.fillPaint = elem.getAttribute('fill') || 'black';
|
||||
currentStyle.fillOpacity = elem.getAttribute('fill-opacity') || 1.0;
|
||||
currentStyle.strokePaint = elem.getAttribute('stroke');
|
||||
currentStyle.strokeOpacity = elem.getAttribute('stroke-opacity') || 1.0;
|
||||
currentStyle.strokeWidth = elem.getAttribute('stroke-width');
|
||||
currentStyle.strokeDashArray = elem.getAttribute('stroke-dasharray');
|
||||
currentStyle.strokeLinecap = elem.getAttribute('stroke-linecap');
|
||||
currentStyle.strokeLinejoin = elem.getAttribute('stroke-linejoin');
|
||||
currentStyle.opacity = elem.getAttribute('opacity') || 1.0;
|
||||
// disable eye-dropper tool
|
||||
} else {
|
||||
tool.addClass('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: 'tool_eyedropper',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'eyedropper.png',
|
||||
type: 'mode',
|
||||
events: {
|
||||
click () {
|
||||
svgCanvas.setMode('eyedropper');
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'eyedropper-icon.xml',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
|
||||
// if we have selected an element, grab its paint and enable the eye dropper button
|
||||
selectedChanged: getStyle,
|
||||
elementChanged: getStyle,
|
||||
|
||||
mouseDown (opts) {
|
||||
const mode = svgCanvas.getMode();
|
||||
if (mode === 'eyedropper') {
|
||||
const e = opts.event;
|
||||
const {target} = e;
|
||||
if (!['svg', 'g', 'use'].includes(target.nodeName)) {
|
||||
const changes = {};
|
||||
|
||||
const change = function (elem, attrname, newvalue) {
|
||||
changes[attrname] = elem.getAttribute(attrname);
|
||||
elem.setAttribute(attrname, newvalue);
|
||||
};
|
||||
|
||||
if (currentStyle.fillPaint) { change(target, 'fill', currentStyle.fillPaint); }
|
||||
if (currentStyle.fillOpacity) { change(target, 'fill-opacity', currentStyle.fillOpacity); }
|
||||
if (currentStyle.strokePaint) { change(target, 'stroke', currentStyle.strokePaint); }
|
||||
if (currentStyle.strokeOpacity) { change(target, 'stroke-opacity', currentStyle.strokeOpacity); }
|
||||
if (currentStyle.strokeWidth) { change(target, 'stroke-width', currentStyle.strokeWidth); }
|
||||
if (currentStyle.strokeDashArray) { change(target, 'stroke-dasharray', currentStyle.strokeDashArray); }
|
||||
if (currentStyle.opacity) { change(target, 'opacity', currentStyle.opacity); }
|
||||
if (currentStyle.strokeLinecap) { change(target, 'stroke-linecap', currentStyle.strokeLinecap); }
|
||||
if (currentStyle.strokeLinejoin) { change(target, 'stroke-linejoin', currentStyle.strokeLinejoin); }
|
||||
|
||||
addToHistory(new ChangeElementCommand(target, changes));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
280
src/editor/extensions/ext-foreignobject.js
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* @file ext-foreignobject.js
|
||||
*
|
||||
* @license Apache-2.0
|
||||
*
|
||||
* @copyright 2010 Jacques Distler, 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'foreignobject',
|
||||
async init (S) {
|
||||
const svgEditor = this;
|
||||
const {$, text2xml, NS, importLocale} = S;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const
|
||||
// {svgcontent} = S,
|
||||
// addElem = svgCanvas.addSVGElementFromJson,
|
||||
svgdoc = S.svgroot.parentNode.ownerDocument;
|
||||
const strings = await importLocale();
|
||||
|
||||
const properlySourceSizeTextArea = function () {
|
||||
// TODO: remove magic numbers here and get values from CSS
|
||||
const height = $('#svg_source_container').height() - 80;
|
||||
$('#svg_source_textarea').css('height', height);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function showPanel (on) {
|
||||
let fcRules = $('#fc_rules');
|
||||
if (!fcRules.length) {
|
||||
fcRules = $('<style id="fc_rules"></style>').appendTo('head');
|
||||
}
|
||||
fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }');
|
||||
$('#foreignObject_panel').toggle(on);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function toggleSourceButtons (on) {
|
||||
$('#tool_source_save, #tool_source_cancel').toggle(!on);
|
||||
$('#foreign_save, #foreign_cancel').toggle(on);
|
||||
}
|
||||
|
||||
let selElems,
|
||||
started,
|
||||
newFO,
|
||||
editingforeign = false;
|
||||
|
||||
/**
|
||||
* This function sets the content of element elt to the input XML.
|
||||
* @param {string} xmlString - The XML text
|
||||
* @returns {boolean} This function returns false if the set was unsuccessful, true otherwise.
|
||||
*/
|
||||
function setForeignString (xmlString) {
|
||||
const elt = selElems[0]; // The parent `Element` to append to
|
||||
try {
|
||||
// convert string into XML document
|
||||
const newDoc = text2xml('<svg xmlns="' + NS.SVG + '" xmlns:xlink="' + NS.XLINK + '">' + xmlString + '</svg>');
|
||||
// run it through our sanitizer to remove anything we do not support
|
||||
svgCanvas.sanitizeSvg(newDoc.documentElement);
|
||||
elt.replaceWith(svgdoc.importNode(newDoc.documentElement.firstChild, true));
|
||||
svgCanvas.call('changed', [elt]);
|
||||
svgCanvas.clearSelection();
|
||||
} catch (e) {
|
||||
// Todo: Surface error to user
|
||||
console.log(e); // eslint-disable-line no-console
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function showForeignEditor () {
|
||||
const elt = selElems[0];
|
||||
if (!elt || editingforeign) { return; }
|
||||
editingforeign = true;
|
||||
toggleSourceButtons(true);
|
||||
elt.removeAttribute('fill');
|
||||
|
||||
const str = svgCanvas.svgToString(elt, 0);
|
||||
$('#svg_source_textarea').val(str);
|
||||
$('#svg_source_editor').fadeIn();
|
||||
properlySourceSizeTextArea();
|
||||
$('#svg_source_textarea').focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} attr
|
||||
* @param {string|Float} val
|
||||
* @returns {void}
|
||||
*/
|
||||
function setAttr (attr, val) {
|
||||
svgCanvas.changeSelectedAttribute(attr, val);
|
||||
svgCanvas.call('changed', selElems);
|
||||
}
|
||||
|
||||
const buttons = [{
|
||||
id: 'tool_foreign',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'foreignobject-tool.png',
|
||||
type: 'mode',
|
||||
events: {
|
||||
click () {
|
||||
svgCanvas.setMode('foreign');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
id: 'edit_foreign',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'foreignobject-edit.png',
|
||||
type: 'context',
|
||||
panel: 'foreignObject_panel',
|
||||
events: {
|
||||
click () {
|
||||
showForeignEditor();
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
const contextTools = [
|
||||
{
|
||||
type: 'input',
|
||||
panel: 'foreignObject_panel',
|
||||
id: 'foreign_width',
|
||||
size: 3,
|
||||
events: {
|
||||
change () {
|
||||
setAttr('width', this.value);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
panel: 'foreignObject_panel',
|
||||
id: 'foreign_height',
|
||||
events: {
|
||||
change () {
|
||||
setAttr('height', this.value);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
panel: 'foreignObject_panel',
|
||||
id: 'foreign_font_size',
|
||||
size: 2,
|
||||
defval: 16,
|
||||
events: {
|
||||
change () {
|
||||
setAttr('font-size', this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'foreignobject-icons.xml',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
context_tools: strings.contextTools.map((contextTool, i) => {
|
||||
return Object.assign(contextTools[i], contextTool);
|
||||
}),
|
||||
callback () {
|
||||
$('#foreignObject_panel').hide();
|
||||
|
||||
const endChanges = function () {
|
||||
$('#svg_source_editor').hide();
|
||||
editingforeign = false;
|
||||
$('#svg_source_textarea').blur();
|
||||
toggleSourceButtons(false);
|
||||
};
|
||||
|
||||
// TODO: Needs to be done after orig icon loads
|
||||
setTimeout(function () {
|
||||
// Create source save/cancel buttons
|
||||
/* const save = */ $('#tool_source_save').clone()
|
||||
.hide().attr('id', 'foreign_save').unbind()
|
||||
.appendTo('#tool_source_back').click(async function () {
|
||||
if (!editingforeign) { return; }
|
||||
|
||||
if (!setForeignString($('#svg_source_textarea').val())) {
|
||||
const ok = await $.confirm('Errors found. Revert to original?');
|
||||
if (!ok) { return; }
|
||||
endChanges();
|
||||
} else {
|
||||
endChanges();
|
||||
}
|
||||
// setSelectMode();
|
||||
});
|
||||
|
||||
/* const cancel = */ $('#tool_source_cancel').clone()
|
||||
.hide().attr('id', 'foreign_cancel').unbind()
|
||||
.appendTo('#tool_source_back').click(function () {
|
||||
endChanges();
|
||||
});
|
||||
}, 3000);
|
||||
},
|
||||
mouseDown (opts) {
|
||||
// const e = opts.event;
|
||||
if (svgCanvas.getMode() !== 'foreign') {
|
||||
return undefined;
|
||||
}
|
||||
started = true;
|
||||
newFO = svgCanvas.addSVGElementFromJson({
|
||||
element: 'foreignObject',
|
||||
attr: {
|
||||
x: opts.start_x,
|
||||
y: opts.start_y,
|
||||
id: svgCanvas.getNextId(),
|
||||
'font-size': 16, // cur_text.font_size,
|
||||
width: '48',
|
||||
height: '20',
|
||||
style: 'pointer-events:inherit'
|
||||
}
|
||||
});
|
||||
const m = svgdoc.createElementNS(NS.MATH, 'math');
|
||||
m.setAttributeNS(NS.XMLNS, 'xmlns', NS.MATH);
|
||||
m.setAttribute('display', 'inline');
|
||||
const mi = svgdoc.createElementNS(NS.MATH, 'mi');
|
||||
mi.setAttribute('mathvariant', 'normal');
|
||||
mi.textContent = '\u03A6';
|
||||
const mo = svgdoc.createElementNS(NS.MATH, 'mo');
|
||||
mo.textContent = '\u222A';
|
||||
const mi2 = svgdoc.createElementNS(NS.MATH, 'mi');
|
||||
mi2.textContent = '\u2133';
|
||||
m.append(mi, mo, mi2);
|
||||
newFO.append(m);
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
},
|
||||
mouseUp (opts) {
|
||||
// const e = opts.event;
|
||||
if (svgCanvas.getMode() !== 'foreign' || !started) {
|
||||
return undefined;
|
||||
}
|
||||
const attrs = $(newFO).attr(['width', 'height']);
|
||||
const keep = (attrs.width !== '0' || attrs.height !== '0');
|
||||
svgCanvas.addToSelection([newFO], true);
|
||||
|
||||
return {
|
||||
keep,
|
||||
element: newFO
|
||||
};
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
// Use this to update the current selected elements
|
||||
selElems = opts.elems;
|
||||
|
||||
let i = selElems.length;
|
||||
while (i--) {
|
||||
const elem = selElems[i];
|
||||
if (elem && elem.tagName === 'foreignObject') {
|
||||
if (opts.selectedElement && !opts.multiselected) {
|
||||
$('#foreign_font_size').val(elem.getAttribute('font-size'));
|
||||
$('#foreign_width').val(elem.getAttribute('width'));
|
||||
$('#foreign_height').val(elem.getAttribute('height'));
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
elementChanged (opts) {
|
||||
// const elem = opts.elems[0];
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
168
src/editor/extensions/ext-grid.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @file ext-grid.js
|
||||
*
|
||||
* @license Apache-2.0
|
||||
*
|
||||
* @copyright 2010 Redou Mine, 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'grid',
|
||||
async init ({$, NS, getTypeMap, importLocale}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const svgdoc = document.getElementById('svgcanvas').ownerDocument,
|
||||
{assignAttributes} = svgCanvas,
|
||||
hcanvas = document.createElement('canvas'),
|
||||
canvBG = $('#canvasBackground'),
|
||||
units = getTypeMap(), // Assumes prior `init()` call on `units.js` module
|
||||
intervals = [0.01, 0.1, 1, 10, 100, 1000];
|
||||
let showGrid = svgEditor.curConfig.showGrid || false;
|
||||
|
||||
$(hcanvas).hide().appendTo('body');
|
||||
|
||||
const canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg');
|
||||
assignAttributes(canvasGrid, {
|
||||
id: 'canvasGrid',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
x: 0,
|
||||
y: 0,
|
||||
overflow: 'visible',
|
||||
display: 'none'
|
||||
});
|
||||
canvBG.append(canvasGrid);
|
||||
const gridDefs = svgdoc.createElementNS(NS.SVG, 'defs');
|
||||
// grid-pattern
|
||||
const gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern');
|
||||
assignAttributes(gridPattern, {
|
||||
id: 'gridpattern',
|
||||
patternUnits: 'userSpaceOnUse',
|
||||
x: 0, // -(value.strokeWidth / 2), // position for strokewidth
|
||||
y: 0, // -(value.strokeWidth / 2), // position for strokewidth
|
||||
width: 100,
|
||||
height: 100
|
||||
});
|
||||
|
||||
const gridimg = svgdoc.createElementNS(NS.SVG, 'image');
|
||||
assignAttributes(gridimg, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100
|
||||
});
|
||||
gridPattern.append(gridimg);
|
||||
gridDefs.append(gridPattern);
|
||||
$('#canvasGrid').append(gridDefs);
|
||||
|
||||
// grid-box
|
||||
const gridBox = svgdoc.createElementNS(NS.SVG, 'rect');
|
||||
assignAttributes(gridBox, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
x: 0,
|
||||
y: 0,
|
||||
'stroke-width': 0,
|
||||
stroke: 'none',
|
||||
fill: 'url(#gridpattern)',
|
||||
style: 'pointer-events: none; display:visible;'
|
||||
});
|
||||
$('#canvasGrid').append(gridBox);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Float} zoom
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateGrid (zoom) {
|
||||
// TODO: Try this with <line> elements, then compare performance difference
|
||||
const unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px
|
||||
const uMulti = unit * zoom;
|
||||
// Calculate the main number interval
|
||||
const rawM = 100 / uMulti;
|
||||
let multi = 1;
|
||||
intervals.some((num) => {
|
||||
multi = num;
|
||||
return rawM <= num;
|
||||
});
|
||||
const bigInt = multi * uMulti;
|
||||
|
||||
// Set the canvas size to the width of the container
|
||||
hcanvas.width = bigInt;
|
||||
hcanvas.height = bigInt;
|
||||
const ctx = hcanvas.getContext('2d');
|
||||
const curD = 0.5;
|
||||
const part = bigInt / 10;
|
||||
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.strokeStyle = svgEditor.curConfig.gridColor;
|
||||
for (let i = 1; i < 10; i++) {
|
||||
const subD = Math.round(part * i) + 0.5;
|
||||
// const lineNum = (i % 2)?12:10;
|
||||
const lineNum = 0;
|
||||
ctx.moveTo(subD, bigInt);
|
||||
ctx.lineTo(subD, lineNum);
|
||||
ctx.moveTo(bigInt, subD);
|
||||
ctx.lineTo(lineNum, subD);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.moveTo(curD, bigInt);
|
||||
ctx.lineTo(curD, 0);
|
||||
|
||||
ctx.moveTo(bigInt, curD);
|
||||
ctx.lineTo(0, curD);
|
||||
ctx.stroke();
|
||||
|
||||
const datauri = hcanvas.toDataURL('image/png');
|
||||
gridimg.setAttribute('width', bigInt);
|
||||
gridimg.setAttribute('height', bigInt);
|
||||
gridimg.parentNode.setAttribute('width', bigInt);
|
||||
gridimg.parentNode.setAttribute('height', bigInt);
|
||||
svgCanvas.setHref(gridimg, datauri);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function gridUpdate () {
|
||||
if (showGrid) {
|
||||
updateGrid(svgCanvas.getZoom());
|
||||
}
|
||||
$('#canvasGrid').toggle(showGrid);
|
||||
$('#view_grid').toggleClass('push_button_pressed tool_button');
|
||||
}
|
||||
const buttons = [{
|
||||
id: 'view_grid',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'grid.png',
|
||||
type: 'context',
|
||||
panel: 'editor_panel',
|
||||
events: {
|
||||
click () {
|
||||
svgEditor.curConfig.showGrid = showGrid = !showGrid;
|
||||
gridUpdate();
|
||||
}
|
||||
}
|
||||
}];
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'grid-icon.xml',
|
||||
|
||||
zoomChanged (zoom) {
|
||||
if (showGrid) { updateGrid(zoom); }
|
||||
},
|
||||
callback () {
|
||||
if (showGrid) {
|
||||
gridUpdate();
|
||||
}
|
||||
},
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
})
|
||||
};
|
||||
}
|
||||
};
|
||||
91
src/editor/extensions/ext-helloworld.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @file ext-helloworld.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a very basic SVG-Edit extension. It adds a "Hello World" button in
|
||||
* the left ("mode") panel. Clicking on the button, and then the canvas
|
||||
* will show the user the point on the canvas that was clicked on.
|
||||
*/
|
||||
export default {
|
||||
name: 'helloworld',
|
||||
async init ({$, importLocale}) {
|
||||
// See `/editor/extensions/ext-locale/helloworld/`
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
return {
|
||||
name: strings.name,
|
||||
// For more notes on how to make an icon file, see the source of
|
||||
// the helloworld-icon.xml
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'helloworld-icon.xml',
|
||||
|
||||
// Multiple buttons can be added in this array
|
||||
buttons: [{
|
||||
// Must match the icon ID in helloworld-icon.xml
|
||||
id: 'hello_world',
|
||||
|
||||
// Fallback, e.g., for `file:///` access
|
||||
icon: svgEditor.curConfig.extIconsPath + 'helloworld.png',
|
||||
|
||||
// This indicates that the button will be added to the "mode"
|
||||
// button panel on the left side
|
||||
type: 'mode',
|
||||
|
||||
// Tooltip text
|
||||
title: strings.buttons[0].title,
|
||||
|
||||
// Events
|
||||
events: {
|
||||
click () {
|
||||
// The action taken when the button is clicked on.
|
||||
// For "mode" buttons, any other button will
|
||||
// automatically be de-pressed.
|
||||
svgCanvas.setMode('hello_world');
|
||||
}
|
||||
}
|
||||
}],
|
||||
// This is triggered when the main mouse button is pressed down
|
||||
// on the editor canvas (not the tool panels)
|
||||
mouseDown () {
|
||||
// Check the mode on mousedown
|
||||
if (svgCanvas.getMode() === 'hello_world') {
|
||||
// The returned object must include "started" with
|
||||
// a value of true in order for mouseUp to be triggered
|
||||
return {started: true};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// This is triggered from anywhere, but "started" must have been set
|
||||
// to true (see above). Note that "opts" is an object with event info
|
||||
mouseUp (opts) {
|
||||
// Check the mode on mouseup
|
||||
if (svgCanvas.getMode() === 'hello_world') {
|
||||
const zoom = svgCanvas.getZoom();
|
||||
|
||||
// Get the actual coordinate by dividing by the zoom value
|
||||
const x = opts.mouse_x / zoom;
|
||||
const y = opts.mouse_y / zoom;
|
||||
|
||||
// We do our own formatting
|
||||
let {text} = strings;
|
||||
[
|
||||
['x', x],
|
||||
['y', y]
|
||||
].forEach(([prop, val]) => {
|
||||
text = text.replace('{' + prop + '}', val);
|
||||
});
|
||||
|
||||
// Show the text using the custom alert function
|
||||
$.alert(text);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
557
src/editor/extensions/ext-imagelib.js
Normal file
@@ -0,0 +1,557 @@
|
||||
/**
|
||||
* @file ext-imagelib.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'imagelib',
|
||||
async init ({$, decode64, importLocale, dropXMLInternalSubset}) {
|
||||
const imagelibStrings = await importLocale();
|
||||
|
||||
const modularVersion = !('svgEditor' in window) ||
|
||||
!window.svgEditor ||
|
||||
window.svgEditor.modules !== false;
|
||||
|
||||
const svgEditor = this;
|
||||
|
||||
const {uiStrings, canvas: svgCanvas, curConfig: {extIconsPath}} = svgEditor;
|
||||
|
||||
imagelibStrings.imgLibs = imagelibStrings.imgLibs.map(({name, url, description}) => {
|
||||
// Todo: Adopt some standard formatting library like `fluent.js` instead
|
||||
url = url
|
||||
// Keep these regexes as is in prep. for switching to `u` flag
|
||||
// which will require escaping
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
.replace(/\{path\}/g, extIconsPath)
|
||||
.replace(
|
||||
// eslint-disable-next-line unicorn/better-regex
|
||||
/\{modularVersion\}/g,
|
||||
modularVersion
|
||||
? (imagelibStrings.moduleEnding || '-es')
|
||||
: ''
|
||||
);
|
||||
return {name, url, description};
|
||||
});
|
||||
const allowedImageLibOrigins = imagelibStrings.imgLibs.map(({url}) => {
|
||||
try {
|
||||
return new URL(url).origin;
|
||||
} catch (err) {
|
||||
return location.origin;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function closeBrowser () {
|
||||
$('#imgbrowse_holder').hide();
|
||||
document.activeElement.blur(); // make sure focus is the body to correct issue #417
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {void}
|
||||
*/
|
||||
function importImage (url) {
|
||||
const newImage = svgCanvas.addSVGElementFromJson({
|
||||
element: 'image',
|
||||
attr: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
id: svgCanvas.getNextId(),
|
||||
style: 'pointer-events:inherit'
|
||||
}
|
||||
});
|
||||
svgCanvas.clearSelection();
|
||||
svgCanvas.addToSelection([newImage]);
|
||||
svgCanvas.setImageURL(url);
|
||||
}
|
||||
|
||||
const pending = {};
|
||||
|
||||
let mode = 's';
|
||||
let multiArr = [];
|
||||
let transferStopped = false;
|
||||
let preview, submit;
|
||||
|
||||
/**
|
||||
* Contains the SVG to insert.
|
||||
* @typedef {PlainObject} ImageLibMessage
|
||||
* @property {"imagelib"} namespace Required to distinguish from any other messages of app.
|
||||
* @property {string} href Set to same value as previous `ImageLibMetaMessage` `id`.
|
||||
* @property {string} data The response (as an SVG string or URL)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Used for setting meta-data before images are retrieved.
|
||||
* @typedef {PlainObject} ImageLibMetaMessage
|
||||
* @property {"imagelib"} namespace Required to distinguish from any other messages of app.
|
||||
* @property {string} name If the subsequent response is an SVG string or if `preview_url`
|
||||
* is present, will be used as the title for the preview image. When an
|
||||
* SVG string is present, will default to the first `<title>`'s contents or
|
||||
* "(SVG #<Length of response>)" if none is present. Otherwise, if `preview_url`
|
||||
* is present, will default to the empty string. Though `name` may be falsy,
|
||||
* it is always expected to be present for meta messages.
|
||||
* @property {string} id Identifier (the expected `href` for a subsequent response message);
|
||||
* used for ensuring the subsequent response can be tied to this `ImageLibMetaMessage` object.
|
||||
* @property {string} [preview_url] When import mode is multiple, used to set an image
|
||||
* source along with the name/title. If the subsequent response is an SVG string
|
||||
* and there is no `preview_url`, the default will just be to show the
|
||||
* name/title. If the response is not an SVG string, the default will be to
|
||||
* show that response (i.e., the URL).
|
||||
* @property {string} entry Set automatically with div holding retrieving
|
||||
* message (until ready to delete)
|
||||
* @todo Should use a separate Map instead of `entry`
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {PlainObject} cfg
|
||||
* @param {string} cfg.origin
|
||||
* @param {ImageLibMetaMessage|ImageLibMessage|string} cfg.data String is deprecated when parsed to JSON `ImageLibMessage`
|
||||
* @returns {void}
|
||||
*/
|
||||
async function onMessage ({origin, data: response}) { // eslint-disable-line no-shadow
|
||||
if (!response || !['string', 'object'].includes(typeof response)) {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
let id;
|
||||
let type;
|
||||
try {
|
||||
// Todo: This block can be removed (and the above check changed to
|
||||
// insist on an object) if embedAPI moves away from a string to
|
||||
// an object (if IE9 support not needed)
|
||||
response = typeof response === 'object' ? response : JSON.parse(response);
|
||||
if (response.namespace !== 'imagelib') {
|
||||
return;
|
||||
}
|
||||
if (!allowedImageLibOrigins.includes('*') && !allowedImageLibOrigins.includes(origin)) {
|
||||
// Todo: Surface this error to user?
|
||||
console.log(`Origin ${origin} not whitelisted for posting to ${window.origin}`); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
const hasName = 'name' in response;
|
||||
const hasHref = 'href' in response;
|
||||
|
||||
if (!hasName && transferStopped) {
|
||||
transferStopped = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasHref) {
|
||||
id = response.href;
|
||||
response = response.data;
|
||||
}
|
||||
|
||||
// Hide possible transfer dialog box
|
||||
$('#dialog_box').hide();
|
||||
type = hasName
|
||||
? 'meta'
|
||||
: response.charAt(0);
|
||||
} catch (e) {
|
||||
// This block is for backward compatibility (for IAN and Openclipart);
|
||||
// should otherwise return
|
||||
if (typeof response === 'string') {
|
||||
const char1 = response.charAt(0);
|
||||
|
||||
if (char1 !== '{' && transferStopped) {
|
||||
transferStopped = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (char1 === '|') {
|
||||
const secondpos = response.indexOf('|', 1);
|
||||
id = response.substr(1, secondpos - 1);
|
||||
response = response.substr(secondpos + 1);
|
||||
type = response.charAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let entry, curMeta, svgStr, imgStr;
|
||||
switch (type) {
|
||||
case 'meta': {
|
||||
// Metadata
|
||||
transferStopped = false;
|
||||
curMeta = response;
|
||||
|
||||
// Should be safe to add dynamic property as passed metadata
|
||||
pending[curMeta.id] = curMeta; // lgtm [js/remote-property-injection]
|
||||
|
||||
const name = (curMeta.name || 'file');
|
||||
|
||||
const message = uiStrings.notification.retrieving.replace('%s', name);
|
||||
|
||||
if (mode !== 'm') {
|
||||
await $.process_cancel(message);
|
||||
transferStopped = true;
|
||||
// Should a message be sent back to the frame?
|
||||
|
||||
$('#dialog_box').hide();
|
||||
} else {
|
||||
entry = $('<div>').text(message).data('id', curMeta.id);
|
||||
preview.append(entry);
|
||||
curMeta.entry = entry;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case '<':
|
||||
svgStr = true;
|
||||
break;
|
||||
case 'd': {
|
||||
if (response.startsWith('data:image/svg+xml')) {
|
||||
const pre = 'data:image/svg+xml;base64,';
|
||||
const src = response.substring(pre.length);
|
||||
response = decode64(src);
|
||||
svgStr = true;
|
||||
break;
|
||||
} else if (response.startsWith('data:image/')) {
|
||||
imgStr = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Else fall through
|
||||
default:
|
||||
// TODO: See if there's a way to base64 encode the binary data stream
|
||||
// const str = 'data:;base64,' + svgedit.utilities.encode64(response, true);
|
||||
|
||||
// Assume it's raw image data
|
||||
// importImage(str);
|
||||
|
||||
// Don't give warning as postMessage may have been used by something else
|
||||
if (mode !== 'm') {
|
||||
closeBrowser();
|
||||
} else {
|
||||
pending[id].entry.remove();
|
||||
}
|
||||
// await $.alert('Unexpected data was returned: ' + response, function() {
|
||||
// if (mode !== 'm') {
|
||||
// closeBrowser();
|
||||
// } else {
|
||||
// pending[id].entry.remove();
|
||||
// }
|
||||
// });
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case 's':
|
||||
// Import one
|
||||
if (svgStr) {
|
||||
svgCanvas.importSvgString(response);
|
||||
} else if (imgStr) {
|
||||
importImage(response);
|
||||
}
|
||||
closeBrowser();
|
||||
break;
|
||||
case 'm': {
|
||||
// Import multiple
|
||||
multiArr.push([(svgStr ? 'svg' : 'img'), response]);
|
||||
curMeta = pending[id];
|
||||
let title;
|
||||
if (svgStr) {
|
||||
if (curMeta && curMeta.name) {
|
||||
title = curMeta.name;
|
||||
} else {
|
||||
// Try to find a title
|
||||
// `dropXMLInternalSubset` is to help prevent the billion laughs attack
|
||||
const xml = new DOMParser().parseFromString(dropXMLInternalSubset(response), 'text/xml').documentElement; // lgtm [js/xml-bomb]
|
||||
title = $(xml).children('title').first().text() || '(SVG #' + response.length + ')';
|
||||
}
|
||||
if (curMeta) {
|
||||
preview.children().each(function () {
|
||||
if ($(this).data('id') === id) {
|
||||
if (curMeta.preview_url) {
|
||||
$(this).html(
|
||||
$('<span>').append(
|
||||
$('<img>').attr('src', curMeta.preview_url),
|
||||
title
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$(this).text(title);
|
||||
}
|
||||
submit.removeAttr('disabled');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
preview.append(
|
||||
$('<div>').text(title)
|
||||
);
|
||||
submit.removeAttr('disabled');
|
||||
}
|
||||
} else {
|
||||
if (curMeta && curMeta.preview_url) {
|
||||
title = curMeta.name || '';
|
||||
entry = $('<span>').append(
|
||||
$('<img>').attr('src', curMeta.preview_url),
|
||||
title
|
||||
);
|
||||
} else {
|
||||
entry = $('<img>').attr('src', response);
|
||||
}
|
||||
|
||||
if (curMeta) {
|
||||
preview.children().each(function () {
|
||||
if ($(this).data('id') === id) {
|
||||
$(this).html(entry);
|
||||
submit.removeAttr('disabled');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
preview.append($('<div>').append(entry));
|
||||
submit.removeAttr('disabled');
|
||||
}
|
||||
}
|
||||
break;
|
||||
} case 'o': {
|
||||
// Open
|
||||
if (!svgStr) { break; }
|
||||
closeBrowser();
|
||||
const ok = await svgEditor.openPrep();
|
||||
if (!ok) { return; }
|
||||
svgCanvas.clear();
|
||||
svgCanvas.setSvgString(response);
|
||||
// updateCanvas();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Receive `postMessage` data
|
||||
window.addEventListener('message', onMessage, true);
|
||||
|
||||
/**
|
||||
* @param {boolean} show
|
||||
* @returns {void}
|
||||
*/
|
||||
function toggleMulti (show) {
|
||||
$('#lib_framewrap, #imglib_opts').css({right: (show ? 200 : 10)});
|
||||
if (!preview) {
|
||||
preview = $('<div id=imglib_preview>').css({
|
||||
position: 'absolute',
|
||||
top: 45,
|
||||
right: 10,
|
||||
width: 180,
|
||||
bottom: 45,
|
||||
background: '#fff',
|
||||
overflow: 'auto'
|
||||
}).insertAfter('#lib_framewrap');
|
||||
|
||||
submit = $('<button disabled>Import selected</button>')
|
||||
.appendTo('#imgbrowse')
|
||||
.on('click touchend', function () {
|
||||
$.each(multiArr, function (i) {
|
||||
const type = this[0];
|
||||
const data = this[1];
|
||||
if (type === 'svg') {
|
||||
svgCanvas.importSvgString(data);
|
||||
} else {
|
||||
importImage(data);
|
||||
}
|
||||
svgCanvas.moveSelectedElements(i * 20, i * 20, false);
|
||||
});
|
||||
preview.empty();
|
||||
multiArr = [];
|
||||
$('#imgbrowse_holder').hide();
|
||||
}).css({
|
||||
position: 'absolute',
|
||||
bottom: 10,
|
||||
right: -10
|
||||
});
|
||||
}
|
||||
|
||||
preview.toggle(show);
|
||||
submit.toggle(show);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function showBrowser () {
|
||||
let browser = $('#imgbrowse');
|
||||
if (!browser.length) {
|
||||
$('<div id=imgbrowse_holder><div id=imgbrowse class=toolbar_button>' +
|
||||
'</div></div>').insertAfter('#svg_docprops');
|
||||
browser = $('#imgbrowse');
|
||||
|
||||
const allLibs = imagelibStrings.select_lib;
|
||||
|
||||
const libOpts = $('<ul id=imglib_opts>').appendTo(browser);
|
||||
const frame = $('<iframe src="javascript:0"/>').prependTo(browser).hide().wrap('<div id=lib_framewrap>');
|
||||
|
||||
const header = $('<h1>').prependTo(browser).text(allLibs).css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
const cancel = $('<button>' + uiStrings.common.cancel + '</button>')
|
||||
.appendTo(browser)
|
||||
.on('click touchend', function () {
|
||||
$('#imgbrowse_holder').hide();
|
||||
}).css({
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
right: -10
|
||||
});
|
||||
|
||||
const leftBlock = $('<span>').css({position: 'absolute', top: 5, left: 10}).appendTo(browser);
|
||||
|
||||
const back = $('<button hidden>' + imagelibStrings.show_list + '</button>')
|
||||
.appendTo(leftBlock)
|
||||
.on('click touchend', function () {
|
||||
frame.attr('src', 'about:blank').hide();
|
||||
libOpts.show();
|
||||
header.text(allLibs);
|
||||
back.hide();
|
||||
}).css({
|
||||
'margin-right': 5
|
||||
}).hide();
|
||||
|
||||
/* const type = */ $('<select><option value=s>' +
|
||||
imagelibStrings.import_single + '</option><option value=m>' +
|
||||
imagelibStrings.import_multi + '</option><option value=o>' +
|
||||
imagelibStrings.open + '</option></select>').appendTo(leftBlock).change(function () {
|
||||
mode = $(this).val();
|
||||
switch (mode) {
|
||||
case 's':
|
||||
case 'o':
|
||||
toggleMulti(false);
|
||||
break;
|
||||
|
||||
case 'm':
|
||||
// Import multiple
|
||||
toggleMulti(true);
|
||||
break;
|
||||
}
|
||||
}).css({
|
||||
'margin-top': 10
|
||||
});
|
||||
|
||||
cancel.prepend($.getSvgIcon('cancel', true));
|
||||
back.prepend($.getSvgIcon('tool_imagelib', true));
|
||||
|
||||
imagelibStrings.imgLibs.forEach(function ({name, url, description}) {
|
||||
$('<li>')
|
||||
.appendTo(libOpts)
|
||||
.text(name)
|
||||
.on('click touchend', function () {
|
||||
frame.attr(
|
||||
'src',
|
||||
url
|
||||
).show();
|
||||
header.text(name);
|
||||
libOpts.hide();
|
||||
back.show();
|
||||
}).append(`<span>${description}</span>`);
|
||||
});
|
||||
} else {
|
||||
$('#imgbrowse_holder').show();
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = [{
|
||||
id: 'tool_imagelib',
|
||||
type: 'app_menu', // _flyout
|
||||
icon: extIconsPath + 'imagelib.png',
|
||||
position: 4,
|
||||
events: {
|
||||
mouseup: showBrowser
|
||||
}
|
||||
}];
|
||||
|
||||
return {
|
||||
svgicons: extIconsPath + 'ext-imagelib.xml',
|
||||
buttons: imagelibStrings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
callback () {
|
||||
$('<style>').text(
|
||||
'#imgbrowse_holder {' +
|
||||
'position: absolute;' +
|
||||
'top: 0;' +
|
||||
'left: 0;' +
|
||||
'width: 100%;' +
|
||||
'height: 100%;' +
|
||||
'background-color: rgba(0, 0, 0, .5);' +
|
||||
'z-index: 5;' +
|
||||
'}' +
|
||||
'#imgbrowse {' +
|
||||
'position: absolute;' +
|
||||
'top: 25px;' +
|
||||
'left: 25px;' +
|
||||
'right: 25px;' +
|
||||
'bottom: 25px;' +
|
||||
'min-width: 300px;' +
|
||||
'min-height: 200px;' +
|
||||
'background: #B0B0B0;' +
|
||||
'border: 1px outset #777;' +
|
||||
'}' +
|
||||
'#imgbrowse h1 {' +
|
||||
'font-size: 20px;' +
|
||||
'margin: .4em;' +
|
||||
'text-align: center;' +
|
||||
'}' +
|
||||
'#lib_framewrap,' +
|
||||
'#imgbrowse > ul {' +
|
||||
'position: absolute;' +
|
||||
'top: 45px;' +
|
||||
'left: 10px;' +
|
||||
'right: 10px;' +
|
||||
'bottom: 10px;' +
|
||||
'background: white;' +
|
||||
'margin: 0;' +
|
||||
'padding: 0;' +
|
||||
'}' +
|
||||
'#imgbrowse > ul {' +
|
||||
'overflow: auto;' +
|
||||
'}' +
|
||||
'#imgbrowse > div {' +
|
||||
'border: 1px solid #666;' +
|
||||
'}' +
|
||||
'#imglib_preview > div {' +
|
||||
'padding: 5px;' +
|
||||
'font-size: 12px;' +
|
||||
'}' +
|
||||
'#imglib_preview img {' +
|
||||
'display: block;' +
|
||||
'margin: 0 auto;' +
|
||||
'max-height: 100px;' +
|
||||
'}' +
|
||||
'#imgbrowse li {' +
|
||||
'list-style: none;' +
|
||||
'padding: .5em;' +
|
||||
'background: #E8E8E8;' +
|
||||
'border-bottom: 1px solid #B0B0B0;' +
|
||||
'line-height: 1.2em;' +
|
||||
'font-style: sans-serif;' +
|
||||
'}' +
|
||||
'#imgbrowse li > span {' +
|
||||
'color: #666;' +
|
||||
'font-size: 15px;' +
|
||||
'display: block;' +
|
||||
'}' +
|
||||
'#imgbrowse li:hover {' +
|
||||
'background: #FFC;' +
|
||||
'cursor: pointer;' +
|
||||
'}' +
|
||||
'#imgbrowse iframe {' +
|
||||
'width: 100%;' +
|
||||
'height: 100%;' +
|
||||
'border: 0;' +
|
||||
'}'
|
||||
).appendTo('head');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
14
src/editor/extensions/ext-imagelib.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="tool_imagelib">
|
||||
<svg width="201" height="211" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path fill="#efe8b8" stroke="#d6c47c" stroke-linecap="round" d="m2.75,49.51761l56.56,-46.26761c12.73,8.25 25.71001,7 46.44,0.75l-56.03999,47.23944l-22.72002,25.01056l-24.23999,-26.73239z" id="svg_2" stroke-width="7"/>
|
||||
<path fill="#a03333" stroke="#3f3f3f" d="m3.75,203.25002c14.33301,7 30.66699,7 46,0l0,-152.00002c-14.66699,8 -32.33301,8 -47,0l1,152.00002zm45.75,-152.25002l56.25,-46.75l0,151l-56,48.00002m-47.25,-154.25002l57.25,-46.5" id="svg_1" stroke-width="7" stroke-linecap="round"/>
|
||||
<path fill="#efe8b8" stroke="#d6c47c" stroke-linecap="round" d="m49.75,49.51801l56.56,-46.26801c12.72998,8.25 25.71002,7 46.44,0.75l-56.03998,47.239l-22.72003,25.011l-24.23999,-26.73199z" stroke-width="7" id="svg_5"/>
|
||||
<path fill="#2f8e2f" stroke="#3f3f3f" d="m50.75,202.25c14.33301,7 30.66699,7.04253 46,0.04253l0,-151.04253c-14.66699,8 -32.33301,8 -47,0l1,151zm45.75,-151.25l56.25,-46.75l0,144.01219l-56,51.98782m-47.25,-151.25002l57.25,-46.5" stroke-width="7" stroke-linecap="round" id="svg_6"/>
|
||||
<path fill="#efe8b8" stroke="#d6c47c" stroke-linecap="round" d="m95.75,49.51801l56.56,-46.26801c12.72998,8.25 25.71002,7 46.44,0.75l-56.03998,47.239l-22.72003,25.011l-24.23999,-26.73199z" stroke-width="7" id="svg_10"/>
|
||||
<path fill="#336393" stroke="#3f3f3f" d="m96.75,200.29445c14.33301,7 30.66699,7 46,0l0,-149.04445c-14.66699,8 -32.33301,8 -47,0l1,149.04445zm45.75,-149.29445l56.25,-46.75l0,148.04445l-56,48m-47.25,-151.29445l57.25,-46.5" stroke-width="7" stroke-linecap="round" id="svg_11"/>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
19
src/editor/extensions/ext-locale/arrows/en.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
name: 'Arrows',
|
||||
langList: [
|
||||
{id: 'arrow_none', textContent: 'No arrow'}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: 'Select arrow type',
|
||||
options: {
|
||||
none: 'No arrow',
|
||||
end: '---->',
|
||||
start: '<----',
|
||||
both: '<--->',
|
||||
mid: '-->--',
|
||||
mid_bk: '--<--'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
19
src/editor/extensions/ext-locale/arrows/fr.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
name: 'Arrows',
|
||||
langList: [
|
||||
{id: 'arrow_none', textContent: 'Sans flèche'}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: 'Select arrow type',
|
||||
options: {
|
||||
none: 'No arrow',
|
||||
end: '---->',
|
||||
start: '<----',
|
||||
both: '<--->',
|
||||
mid: '-->--',
|
||||
mid_bk: '--<--'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
19
src/editor/extensions/ext-locale/arrows/zh-CN.js
Executable file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
name: '箭头',
|
||||
langList: [
|
||||
{id: 'arrow_none', textContent: '无箭头'}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: '选择箭头类型',
|
||||
options: {
|
||||
none: '无箭头',
|
||||
end: '---->',
|
||||
start: '<----',
|
||||
both: '<--->',
|
||||
mid: '-->--',
|
||||
mid_bk: '--<--'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
11
src/editor/extensions/ext-locale/closepath/en.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
name: 'ClosePath',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Open path'
|
||||
},
|
||||
{
|
||||
title: 'Close path'
|
||||
}
|
||||
]
|
||||
};
|
||||
11
src/editor/extensions/ext-locale/closepath/zh-CN.js
Executable file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
name: '闭合路径',
|
||||
buttons: [
|
||||
{
|
||||
title: '打开路径'
|
||||
},
|
||||
{
|
||||
title: '关闭路径'
|
||||
}
|
||||
]
|
||||
};
|
||||
11
src/editor/extensions/ext-locale/connector/en.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
name: 'Connector',
|
||||
langList: [
|
||||
{id: 'mode_connect', title: 'Connect two objects'}
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
title: 'Connect two objects'
|
||||
}
|
||||
]
|
||||
};
|
||||
11
src/editor/extensions/ext-locale/connector/fr.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
name: 'Connector',
|
||||
langList: [
|
||||
{id: 'mode_connect', title: 'Connecter deux objets'}
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
title: 'Connect two objects'
|
||||
}
|
||||
]
|
||||
};
|
||||
11
src/editor/extensions/ext-locale/connector/zh-CN.js
Executable file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
name: '连接器',
|
||||
langList: [
|
||||
{id: 'mode_connect', title: '连接两个对象'}
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
title: '连接两个对象'
|
||||
}
|
||||
]
|
||||
};
|
||||
9
src/editor/extensions/ext-locale/eyedropper/en.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
name: 'eyedropper',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Eye Dropper Tool',
|
||||
key: 'I'
|
||||
}
|
||||
]
|
||||
};
|
||||
9
src/editor/extensions/ext-locale/eyedropper/zh-CN.js
Executable file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
name: '滴管',
|
||||
buttons: [
|
||||
{
|
||||
title: '滴管工具',
|
||||
key: 'I'
|
||||
}
|
||||
]
|
||||
};
|
||||
25
src/editor/extensions/ext-locale/foreignobject/en.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
name: 'foreignObject',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Foreign Object Tool'
|
||||
},
|
||||
{
|
||||
title: 'Edit ForeignObject Content'
|
||||
}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: "Change foreignObject's width",
|
||||
label: 'w'
|
||||
},
|
||||
{
|
||||
title: "Change foreignObject's height",
|
||||
label: 'h'
|
||||
},
|
||||
{
|
||||
title: "Change foreignObject's font size",
|
||||
label: 'font-size'
|
||||
}
|
||||
]
|
||||
};
|
||||
25
src/editor/extensions/ext-locale/foreignobject/zh-CN.js
Executable file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
name: '外部对象',
|
||||
buttons: [
|
||||
{
|
||||
title: '外部对象工具'
|
||||
},
|
||||
{
|
||||
title: '编辑外部对象内容'
|
||||
}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: '改变外部对象宽度',
|
||||
label: 'w'
|
||||
},
|
||||
{
|
||||
title: '改变外部对象高度',
|
||||
label: 'h'
|
||||
},
|
||||
{
|
||||
title: '改变外部对象文字大小',
|
||||
label: '文字大小'
|
||||
}
|
||||
]
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/grid/en.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: 'View Grid',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Show/Hide Grid'
|
||||
}
|
||||
]
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/grid/zh-CN.js
Executable file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: '网格视图',
|
||||
buttons: [
|
||||
{
|
||||
title: '显示/隐藏网格'
|
||||
}
|
||||
]
|
||||
};
|
||||
9
src/editor/extensions/ext-locale/helloworld/en.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
name: 'Hello World',
|
||||
text: 'Hello World!\n\nYou clicked here: {x}, {y}',
|
||||
buttons: [
|
||||
{
|
||||
title: "Say 'Hello World'"
|
||||
}
|
||||
]
|
||||
};
|
||||
9
src/editor/extensions/ext-locale/helloworld/zh-CN.js
Executable file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
name: 'Hello World',
|
||||
text: 'Hello World!\n\n 请点击: {x}, {y}',
|
||||
buttons: [
|
||||
{
|
||||
title: "输出 'Hello World'"
|
||||
}
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/de.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Bilder-Bibliothek'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
38
src/editor/extensions/ext-locale/imagelib/en.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Image library'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
// The site is no longer using our API, and they have added an
|
||||
// `X-Frame-Options` header which prevents our usage cross-origin:
|
||||
// Getting messages like this in console:
|
||||
// Refused to display 'https://openclipart.org/detail/307176/sign-bike' in a frame
|
||||
// because it set 'X-Frame-Options' to 'sameorigin'.
|
||||
// url: 'https://openclipart.org/svgedit',
|
||||
// However, they do have a custom API which we are using here:
|
||||
/*
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: '{path}imagelib/openclipart{modularVersion}.html',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/fr.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: "Choisir une bibliothèque d'images",
|
||||
show_list: 'show_list',
|
||||
import_single: 'import_single',
|
||||
import_multi: 'import_multi',
|
||||
open: 'open',
|
||||
buttons: [
|
||||
{
|
||||
title: "Bibliothèque d'images"
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/pl.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Biblioteka obrazów'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/pt-BR.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Biblioteca de Imagens'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/ro.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Bibliotecă de Imagini'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/sk.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Knižnica obrázkov'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/sl.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Knjižnica slik'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
33
src/editor/extensions/ext-locale/imagelib/zh-CN.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
select_lib: 'Select an image library',
|
||||
show_list: 'Show library list',
|
||||
import_single: 'Import single',
|
||||
import_multi: 'Import multiple',
|
||||
open: 'Open as new document',
|
||||
buttons: [
|
||||
{
|
||||
title: '图像库'
|
||||
}
|
||||
],
|
||||
imgLibs: [
|
||||
{
|
||||
name: 'Demo library (local)',
|
||||
url: '{path}imagelib/index{modularVersion}.html',
|
||||
description: 'Demonstration library for SVG-edit on this server'
|
||||
},
|
||||
{
|
||||
name: 'IAN Symbol Libraries',
|
||||
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
|
||||
description: 'Free library of illustrations'
|
||||
}
|
||||
/*
|
||||
// See message in "en" locale for further details
|
||||
,
|
||||
{
|
||||
name: 'Openclipart',
|
||||
url: 'https://openclipart.org/svgedit',
|
||||
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
|
||||
}
|
||||
*/
|
||||
]
|
||||
};
|
||||
46
src/editor/extensions/ext-locale/markers/en.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export default {
|
||||
name: 'Markers',
|
||||
langList: [
|
||||
{id: 'nomarker', title: 'No Marker'},
|
||||
{id: 'leftarrow', title: 'Left Arrow'},
|
||||
{id: 'rightarrow', title: 'Right Arrow'},
|
||||
{id: 'textmarker', title: 'Text Marker'},
|
||||
{id: 'forwardslash', title: 'Forward Slash'},
|
||||
{id: 'reverseslash', title: 'Reverse Slash'},
|
||||
{id: 'verticalslash', title: 'Vertical Slash'},
|
||||
{id: 'box', title: 'Box'},
|
||||
{id: 'star', title: 'Star'},
|
||||
{id: 'xmark', title: 'X'},
|
||||
{id: 'triangle', title: 'Triangle'},
|
||||
{id: 'mcircle', title: 'Circle'},
|
||||
{id: 'leftarrow_o', title: 'Open Left Arrow'},
|
||||
{id: 'rightarrow_o', title: 'Open Right Arrow'},
|
||||
{id: 'box_o', title: 'Open Box'},
|
||||
{id: 'star_o', title: 'Open Star'},
|
||||
{id: 'triangle_o', title: 'Open Triangle'},
|
||||
{id: 'mcircle_o', title: 'Open Circle'}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: 'Start marker',
|
||||
label: 's'
|
||||
},
|
||||
{
|
||||
title: 'Select start marker type'
|
||||
},
|
||||
{
|
||||
title: 'Middle marker',
|
||||
label: 'm'
|
||||
},
|
||||
{
|
||||
title: 'Select mid marker type'
|
||||
},
|
||||
{
|
||||
title: 'End marker',
|
||||
label: 'e'
|
||||
},
|
||||
{
|
||||
title: 'Select end marker type'
|
||||
}
|
||||
]
|
||||
};
|
||||
46
src/editor/extensions/ext-locale/markers/zh-CN.js
Executable file
@@ -0,0 +1,46 @@
|
||||
export default {
|
||||
name: '标记',
|
||||
langList: [
|
||||
{id: 'nomarker', title: '无标记'},
|
||||
{id: 'leftarrow', title: '左箭头'},
|
||||
{id: 'rightarrow', title: '右箭头'},
|
||||
{id: 'textmarker', title: '文本'},
|
||||
{id: 'forwardslash', title: '斜杠'},
|
||||
{id: 'reverseslash', title: '反斜杠'},
|
||||
{id: 'verticalslash', title: '垂直线'},
|
||||
{id: 'box', title: '方块'},
|
||||
{id: 'star', title: '星形'},
|
||||
{id: 'xmark', title: 'X'},
|
||||
{id: 'triangle', title: '三角形'},
|
||||
{id: 'mcircle', title: '圆形'},
|
||||
{id: 'leftarrow_o', title: '左箭头(空心)'},
|
||||
{id: 'rightarrow_o', title: '右箭头(空心)'},
|
||||
{id: 'box_o', title: '方块(空心)'},
|
||||
{id: 'star_o', title: '星形(空心)'},
|
||||
{id: 'triangle_o', title: '三角形(空心)'},
|
||||
{id: 'mcircle_o', title: '圆形(空心)'}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: '起始标记',
|
||||
label: 's'
|
||||
},
|
||||
{
|
||||
title: '选择起始标记类型'
|
||||
},
|
||||
{
|
||||
title: '中段标记',
|
||||
label: 'm'
|
||||
},
|
||||
{
|
||||
title: '选择中段标记类型'
|
||||
},
|
||||
{
|
||||
title: '末端标记',
|
||||
label: 'e'
|
||||
},
|
||||
{
|
||||
title: '选择末端标记类型'
|
||||
}
|
||||
]
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/mathjax/en.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: 'MathJax',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Add Mathematics'
|
||||
}
|
||||
]
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/mathjax/zh-CN.js
Executable file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: '数学',
|
||||
buttons: [
|
||||
{
|
||||
title: '添加数学计算'
|
||||
}
|
||||
]
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/panning/en.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: 'Extension Panning',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Panning'
|
||||
}
|
||||
]
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/panning/zh-CN.js
Executable file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: '移动',
|
||||
buttons: [
|
||||
{
|
||||
title: '移动'
|
||||
}
|
||||
]
|
||||
};
|
||||
40
src/editor/extensions/ext-locale/placemark/en.js
Normal file
@@ -0,0 +1,40 @@
|
||||
export default {
|
||||
name: 'placemark',
|
||||
langList: [
|
||||
{id: 'nomarker', title: 'No Marker'},
|
||||
{id: 'leftarrow', title: 'Left Arrow'},
|
||||
{id: 'rightarrow', title: 'Right Arrow'},
|
||||
{id: 'forwardslash', title: 'Forward Slash'},
|
||||
{id: 'reverseslash', title: 'Reverse Slash'},
|
||||
{id: 'verticalslash', title: 'Vertical Slash'},
|
||||
{id: 'box', title: 'Box'},
|
||||
{id: 'star', title: 'Star'},
|
||||
{id: 'xmark', title: 'X'},
|
||||
{id: 'triangle', title: 'Triangle'},
|
||||
{id: 'mcircle', title: 'Circle'},
|
||||
{id: 'leftarrow_o', title: 'Open Left Arrow'},
|
||||
{id: 'rightarrow_o', title: 'Open Right Arrow'},
|
||||
{id: 'box_o', title: 'Open Box'},
|
||||
{id: 'star_o', title: 'Open Star'},
|
||||
{id: 'triangle_o', title: 'Open Triangle'},
|
||||
{id: 'mcircle_o', title: 'Open Circle'}
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
title: 'Placemark Tool'
|
||||
}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: 'Select Place marker type'
|
||||
},
|
||||
{
|
||||
title: 'Text on separated with ; ',
|
||||
label: 'Text'
|
||||
},
|
||||
{
|
||||
title: 'Font for text',
|
||||
label: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
14
src/editor/extensions/ext-locale/polygon/en.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
name: 'polygon',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Polygon Tool'
|
||||
}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: 'Number of Sides',
|
||||
label: 'sides'
|
||||
}
|
||||
]
|
||||
};
|
||||
14
src/editor/extensions/ext-locale/polygon/zh-CN.js
Executable file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
name: '多边形',
|
||||
buttons: [
|
||||
{
|
||||
title: '多边形工具'
|
||||
}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: '边数',
|
||||
label: '边数'
|
||||
}
|
||||
]
|
||||
};
|
||||
4
src/editor/extensions/ext-locale/server_moinsave/en.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
saved: 'Saved! Return to Item View!',
|
||||
hiddenframe: 'Moinsave frame to store hidden values'
|
||||
};
|
||||
4
src/editor/extensions/ext-locale/server_moinsave/zh-CN.js
Executable file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
saved: '已保存! 返回视图!',
|
||||
hiddenframe: 'Moinsave frame to store hidden values'
|
||||
};
|
||||
4
src/editor/extensions/ext-locale/server_opensave/en.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
uploading: 'Uploading...',
|
||||
hiddenframe: 'Opensave frame to store hidden values'
|
||||
};
|
||||
4
src/editor/extensions/ext-locale/server_opensave/zh-CN.js
Executable file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
uploading: '正在上传...',
|
||||
hiddenframe: 'Opensave frame to store hidden values'
|
||||
};
|
||||
24
src/editor/extensions/ext-locale/shapes/en.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
loading: 'Loading...',
|
||||
categories: {
|
||||
basic: 'Basic',
|
||||
object: 'Objects',
|
||||
symbol: 'Symbols',
|
||||
arrow: 'Arrows',
|
||||
flowchart: 'Flowchart',
|
||||
animal: 'Animals',
|
||||
game: 'Cards & Chess',
|
||||
dialog_balloon: 'Dialog balloons',
|
||||
electronics: 'Electronics',
|
||||
math: 'Mathematical',
|
||||
music: 'Music',
|
||||
misc: 'Miscellaneous',
|
||||
raphael_1: 'raphaeljs.com set 1',
|
||||
raphael_2: 'raphaeljs.com set 2'
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
title: 'Shape library'
|
||||
}
|
||||
]
|
||||
};
|
||||
24
src/editor/extensions/ext-locale/shapes/fr.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
loading: 'Loading...',
|
||||
categories: {
|
||||
basic: 'Basic',
|
||||
object: 'Objects',
|
||||
symbol: 'Symbols',
|
||||
arrow: 'Arrows',
|
||||
flowchart: 'Flowchart',
|
||||
animal: 'Animals',
|
||||
game: 'Cards & Chess',
|
||||
dialog_balloon: 'Dialog balloons',
|
||||
electronics: 'Electronics',
|
||||
math: 'Mathematical',
|
||||
music: 'Music',
|
||||
misc: 'Miscellaneous',
|
||||
raphael_1: 'raphaeljs.com set 1',
|
||||
raphael_2: 'raphaeljs.com set 2'
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
title: "Bibliothèque d'images"
|
||||
}
|
||||
]
|
||||
};
|
||||
24
src/editor/extensions/ext-locale/shapes/zh-CN.js
Executable file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
loading: '正在加载...',
|
||||
categories: {
|
||||
basic: '基本',
|
||||
object: '对象',
|
||||
symbol: '符号',
|
||||
arrow: '箭头',
|
||||
flowchart: '工作流',
|
||||
animal: '动物',
|
||||
game: '棋牌',
|
||||
dialog_balloon: '会话框',
|
||||
electronics: '电子',
|
||||
math: '数学',
|
||||
music: '音乐',
|
||||
misc: '其他',
|
||||
raphael_1: 'raphaeljs.com 集合 1',
|
||||
raphael_2: 'raphaeljs.com 集合 2'
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
title: '图元库'
|
||||
}
|
||||
]
|
||||
};
|
||||
22
src/editor/extensions/ext-locale/star/en.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default {
|
||||
name: 'star',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Star Tool'
|
||||
}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: 'Number of Sides',
|
||||
label: 'points'
|
||||
},
|
||||
{
|
||||
title: 'Pointiness',
|
||||
label: 'Pointiness'
|
||||
},
|
||||
{
|
||||
title: 'Twists the star',
|
||||
label: 'Radial Shift'
|
||||
}
|
||||
]
|
||||
};
|
||||
22
src/editor/extensions/ext-locale/star/zh-CN.js
Executable file
@@ -0,0 +1,22 @@
|
||||
export default {
|
||||
name: '星形',
|
||||
buttons: [
|
||||
{
|
||||
title: '星形工具'
|
||||
}
|
||||
],
|
||||
contextTools: [
|
||||
{
|
||||
title: '顶点',
|
||||
label: '顶点'
|
||||
},
|
||||
{
|
||||
title: '钝度',
|
||||
label: '钝度'
|
||||
},
|
||||
{
|
||||
title: '径向',
|
||||
label: '径向'
|
||||
}
|
||||
]
|
||||
};
|
||||
14
src/editor/extensions/ext-locale/storage/de.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
message: 'Standardmäßig kann SVG-Edit Ihre Editor-Einstellungen ' +
|
||||
'und die SVG-Inhalte lokal auf Ihrem Gerät abspeichern. So brauchen Sie ' +
|
||||
'nicht jedes Mal die SVG neu laden. Falls Sie aus Datenschutzgründen ' +
|
||||
'dies nicht wollen, ' +
|
||||
'können Sie die Standardeinstellung im Folgenden ändern.',
|
||||
storagePrefsAndContent: 'Store preferences and SVG content locally',
|
||||
storagePrefsOnly: 'Only store preferences locally',
|
||||
storagePrefs: 'Store preferences locally',
|
||||
storageNoPrefsOrContent: 'Do not store my preferences or SVG content locally',
|
||||
storageNoPrefs: 'Do not store my preferences locally',
|
||||
rememberLabel: 'Remember this choice?',
|
||||
rememberTooltip: 'If you choose to opt out of storage while remembering this choice, the URL will change so as to avoid asking again.'
|
||||
};
|
||||
14
src/editor/extensions/ext-locale/storage/en.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
message: 'By default and where supported, SVG-Edit can store your editor ' +
|
||||
'preferences and SVG content locally on your machine so you do not ' +
|
||||
'need to add these back each time you load SVG-Edit. If, for privacy ' +
|
||||
'reasons, you do not wish to store this information on your machine, ' +
|
||||
'you can change away from the default option below.',
|
||||
storagePrefsAndContent: 'Store preferences and SVG content locally',
|
||||
storagePrefsOnly: 'Only store preferences locally',
|
||||
storagePrefs: 'Store preferences locally',
|
||||
storageNoPrefsOrContent: 'Do not store my preferences or SVG content locally',
|
||||
storageNoPrefs: 'Do not store my preferences locally',
|
||||
rememberLabel: 'Remember this choice?',
|
||||
rememberTooltip: 'If you choose to opt out of storage while remembering this choice, the URL will change so as to avoid asking again.'
|
||||
};
|
||||
14
src/editor/extensions/ext-locale/storage/fr.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
message: "Par défaut et si supporté, SVG-Edit peut stocker les préférences de l'éditeur " +
|
||||
"et le contenu SVG localement sur votre machine de sorte que vous n'ayez pas besoin de les " +
|
||||
'rajouter chaque fois que vous chargez SVG-Edit. Si, pour des raisons de confidentialité, ' +
|
||||
'vous ne souhaitez pas stocker ces données sur votre machine, vous pouvez changer ce ' +
|
||||
'comportement ci-dessous.',
|
||||
storagePrefsAndContent: 'Store preferences and SVG content locally',
|
||||
storagePrefsOnly: 'Only store preferences locally',
|
||||
storagePrefs: 'Store preferences locally',
|
||||
storageNoPrefsOrContent: 'Do not store my preferences or SVG content locally',
|
||||
storageNoPrefs: 'Do not store my preferences locally',
|
||||
rememberLabel: 'Remember this choice?',
|
||||
rememberTooltip: "Si vous choisissez de désactiver le stockage en mémorisant le choix, l'URL va changer afin que la question ne vous soit plus reposée."
|
||||
};
|
||||
11
src/editor/extensions/ext-locale/storage/zh-CN.js
Executable file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
message: '默认情况下, SVG-Edit 在本地保存配置参数和画布内容. 如果基于隐私考虑, ' +
|
||||
'您可以勾选以下选项修改配置.',
|
||||
storagePrefsAndContent: '本地存储配置参数和SVG图',
|
||||
storagePrefsOnly: '本地只存储配置参数',
|
||||
storagePrefs: '本地存储配置参数',
|
||||
storageNoPrefsOrContent: '本地不保存配置参数和SVG图',
|
||||
storageNoPrefs: '本地不保存配置参数',
|
||||
rememberLabel: '记住选择?',
|
||||
rememberTooltip: '如果您勾选记住选择,将不再弹出本窗口.'
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/webappfind/en.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: 'WebAppFind',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Save Image back to Disk'
|
||||
}
|
||||
]
|
||||
};
|
||||
8
src/editor/extensions/ext-locale/webappfind/zh-CN.js
Executable file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: 'WebAppFind',
|
||||
buttons: [
|
||||
{
|
||||
title: '保存图片到磁盘'
|
||||
}
|
||||
]
|
||||
};
|
||||
602
src/editor/extensions/ext-markers.js
Normal file
@@ -0,0 +1,602 @@
|
||||
/**
|
||||
* @file ext-markers.js
|
||||
*
|
||||
* @license Apache-2.0
|
||||
*
|
||||
* @copyright 2010 Will Schleter based on ext-arrows.js by Copyright(c) 2010 Alexis Deveria
|
||||
*
|
||||
* This extension provides for the addition of markers to the either end
|
||||
* or the middle of a line, polyline, path, polygon.
|
||||
*
|
||||
* Markers may be either a graphic or arbitary text
|
||||
*
|
||||
* to simplify the coding and make the implementation as robust as possible,
|
||||
* markers are not shared - every object has its own set of markers.
|
||||
* this relationship is maintained by a naming convention between the
|
||||
* ids of the markers and the ids of the object
|
||||
*
|
||||
* The following restrictions exist for simplicty of use and programming
|
||||
* objects and their markers to have the same color
|
||||
* marker size is fixed
|
||||
* text marker font, size, and attributes are fixed
|
||||
* an application specific attribute - se_type - is added to each marker element
|
||||
* to store the type of marker
|
||||
*
|
||||
* @todo
|
||||
* remove some of the restrictions above
|
||||
* add option for keeping text aligned to horizontal
|
||||
* add support for dimension extension lines
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'markers',
|
||||
async init (S) {
|
||||
const strings = await S.importLocale();
|
||||
const svgEditor = this;
|
||||
const {$} = S;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const // {svgcontent} = S,
|
||||
addElem = svgCanvas.addSVGElementFromJson;
|
||||
const mtypes = ['start', 'mid', 'end'];
|
||||
const markerPrefix = 'se_marker_';
|
||||
const idPrefix = 'mkr_';
|
||||
|
||||
// note - to add additional marker types add them below with a unique id
|
||||
// and add the associated icon(s) to marker-icons.svg
|
||||
// the geometry is normalized to a 100x100 box with the origin at lower left
|
||||
// Safari did not like negative values for low left of viewBox
|
||||
// remember that the coordinate system has +y downward
|
||||
const markerTypes = {
|
||||
nomarker: {},
|
||||
leftarrow:
|
||||
{element: 'path', attr: {d: 'M0,50 L100,90 L70,50 L100,10 Z'}},
|
||||
rightarrow:
|
||||
{element: 'path', attr: {d: 'M100,50 L0,90 L30,50 L0,10 Z'}},
|
||||
textmarker:
|
||||
{element: 'text', attr: {x: 0, y: 0, 'stroke-width': 0, stroke: 'none', 'font-size': 75, 'font-family': 'serif', 'text-anchor': 'left',
|
||||
'xml:space': 'preserve'}},
|
||||
forwardslash:
|
||||
{element: 'path', attr: {d: 'M30,100 L70,0'}},
|
||||
reverseslash:
|
||||
{element: 'path', attr: {d: 'M30,0 L70,100'}},
|
||||
verticalslash:
|
||||
{element: 'path', attr: {d: 'M50,0 L50,100'}},
|
||||
box:
|
||||
{element: 'path', attr: {d: 'M20,20 L20,80 L80,80 L80,20 Z'}},
|
||||
star:
|
||||
{element: 'path', attr: {d: 'M10,30 L90,30 L20,90 L50,10 L80,90 Z'}},
|
||||
xmark:
|
||||
{element: 'path', attr: {d: 'M20,80 L80,20 M80,80 L20,20'}},
|
||||
triangle:
|
||||
{element: 'path', attr: {d: 'M10,80 L50,20 L80,80 Z'}},
|
||||
mcircle:
|
||||
{element: 'circle', attr: {r: 30, cx: 50, cy: 50}}
|
||||
};
|
||||
|
||||
// duplicate shapes to support unfilled (open) marker types with an _o suffix
|
||||
['leftarrow', 'rightarrow', 'box', 'star', 'mcircle', 'triangle'].forEach((v) => {
|
||||
markerTypes[v + '_o'] = markerTypes[v];
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Element} elem - A graphic element will have an attribute like marker-start
|
||||
* @param {"marker-start"|"marker-mid"|"marker-end"} attr
|
||||
* @returns {Element} The marker element that is linked to the graphic element
|
||||
*/
|
||||
function getLinked (elem, attr) {
|
||||
const str = elem.getAttribute(attr);
|
||||
if (!str) { return null; }
|
||||
const m = str.match(/\(#(.*)\)/);
|
||||
// const m = str.match(/\(#(?<id>.+)\)/);
|
||||
// if (!m || !m.groups.id) {
|
||||
if (!m || m.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
return svgCanvas.getElem(m[1]);
|
||||
// return svgCanvas.getElem(m.groups.id);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"start"|"mid"|"end"} pos
|
||||
* @param {string} id
|
||||
* @returns {void}
|
||||
*/
|
||||
function setIcon (pos, id) {
|
||||
if (id.substr(0, 1) !== '\\') { id = '\\textmarker'; }
|
||||
const ci = '#' + idPrefix + pos + '_' + id.substr(1);
|
||||
svgEditor.setIcon('#cur_' + pos + '_marker_list', $(ci).children());
|
||||
$(ci).addClass('current').siblings().removeClass('current');
|
||||
}
|
||||
|
||||
let selElems;
|
||||
/**
|
||||
* Toggles context tool panel off/on. Sets the controls with the
|
||||
* selected element's settings.
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function showPanel (on) {
|
||||
$('#marker_panel').toggle(on);
|
||||
|
||||
if (on) {
|
||||
const el = selElems[0];
|
||||
|
||||
let val, ci;
|
||||
$.each(mtypes, function (i, pos) {
|
||||
const m = getLinked(el, 'marker-' + pos);
|
||||
const txtbox = $('#' + pos + '_marker');
|
||||
if (!m) {
|
||||
val = '\\nomarker';
|
||||
ci = val;
|
||||
txtbox.hide(); // hide text box
|
||||
} else {
|
||||
if (!m.attributes.se_type) { return; } // not created by this extension
|
||||
val = '\\' + m.attributes.se_type.textContent;
|
||||
ci = val;
|
||||
if (val === '\\textmarker') {
|
||||
val = m.lastChild.textContent;
|
||||
// txtbox.show(); // show text box
|
||||
} else {
|
||||
txtbox.hide(); // hide text box
|
||||
}
|
||||
}
|
||||
txtbox.val(val);
|
||||
setIcon(pos, ci);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {""|"\\nomarker"|"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} val
|
||||
* @returns {SVGMarkerElement}
|
||||
*/
|
||||
function addMarker (id, val) {
|
||||
const txtBoxBg = '#ffffff';
|
||||
const txtBoxBorder = 'none';
|
||||
const txtBoxStrokeWidth = 0;
|
||||
|
||||
let marker = svgCanvas.getElem(id);
|
||||
if (marker) { return undefined; }
|
||||
|
||||
if (val === '' || val === '\\nomarker') { return undefined; }
|
||||
|
||||
const el = selElems[0];
|
||||
const color = el.getAttribute('stroke');
|
||||
// NOTE: Safari didn't like a negative value in viewBox
|
||||
// so we use a standardized 0 0 100 100
|
||||
// with 50 50 being mapped to the marker position
|
||||
const strokeWidth = 10;
|
||||
let refX = 50;
|
||||
let refY = 50;
|
||||
let viewBox = '0 0 100 100';
|
||||
let markerWidth = 5;
|
||||
let markerHeight = 5;
|
||||
let seType;
|
||||
if (val.substr(0, 1) === '\\') {
|
||||
seType = val.substr(1);
|
||||
} else { seType = 'textmarker'; }
|
||||
|
||||
if (!markerTypes[seType]) { return undefined; } // an unknown type!
|
||||
|
||||
// create a generic marker
|
||||
marker = addElem({
|
||||
element: 'marker',
|
||||
attr: {
|
||||
id,
|
||||
markerUnits: 'strokeWidth',
|
||||
orient: 'auto',
|
||||
style: 'pointer-events:none',
|
||||
se_type: seType
|
||||
}
|
||||
});
|
||||
|
||||
if (seType !== 'textmarker') {
|
||||
const mel = addElem(markerTypes[seType]);
|
||||
const fillcolor = (seType.substr(-2) === '_o')
|
||||
? 'none'
|
||||
: color;
|
||||
|
||||
mel.setAttribute('fill', fillcolor);
|
||||
mel.setAttribute('stroke', color);
|
||||
mel.setAttribute('stroke-width', strokeWidth);
|
||||
marker.append(mel);
|
||||
} else {
|
||||
const text = addElem(markerTypes[seType]);
|
||||
// have to add text to get bounding box
|
||||
text.textContent = val;
|
||||
const tb = text.getBBox();
|
||||
// alert(tb.x + ' ' + tb.y + ' ' + tb.width + ' ' + tb.height);
|
||||
const pad = 1;
|
||||
const bb = tb;
|
||||
bb.x = 0;
|
||||
bb.y = 0;
|
||||
bb.width += pad * 2;
|
||||
bb.height += pad * 2;
|
||||
// shift text according to its size
|
||||
text.setAttribute('x', pad);
|
||||
text.setAttribute('y', bb.height - pad - tb.height / 4); // kludge?
|
||||
text.setAttribute('fill', color);
|
||||
refX = bb.width / 2 + pad;
|
||||
refY = bb.height / 2 + pad;
|
||||
viewBox = bb.x + ' ' + bb.y + ' ' + bb.width + ' ' + bb.height;
|
||||
markerWidth = bb.width / 10;
|
||||
markerHeight = bb.height / 10;
|
||||
|
||||
const box = addElem({
|
||||
element: 'rect',
|
||||
attr: {
|
||||
x: bb.x,
|
||||
y: bb.y,
|
||||
width: bb.width,
|
||||
height: bb.height,
|
||||
fill: txtBoxBg,
|
||||
stroke: txtBoxBorder,
|
||||
'stroke-width': txtBoxStrokeWidth
|
||||
}
|
||||
});
|
||||
marker.setAttribute('orient', 0);
|
||||
marker.append(box, text);
|
||||
}
|
||||
|
||||
marker.setAttribute('viewBox', viewBox);
|
||||
marker.setAttribute('markerWidth', markerWidth);
|
||||
marker.setAttribute('markerHeight', markerHeight);
|
||||
marker.setAttribute('refX', refX);
|
||||
marker.setAttribute('refY', refY);
|
||||
svgCanvas.findDefs().append(marker);
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} elem
|
||||
* @returns {SVGPolylineElement}
|
||||
*/
|
||||
function convertline (elem) {
|
||||
// this routine came from the connectors extension
|
||||
// it is needed because midpoint markers don't work with line elements
|
||||
if (elem.tagName !== 'line') { return elem; }
|
||||
|
||||
// Convert to polyline to accept mid-arrow
|
||||
|
||||
const x1 = Number(elem.getAttribute('x1'));
|
||||
const x2 = Number(elem.getAttribute('x2'));
|
||||
const y1 = Number(elem.getAttribute('y1'));
|
||||
const y2 = Number(elem.getAttribute('y2'));
|
||||
const {id} = elem;
|
||||
|
||||
const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' ');
|
||||
const pline = addElem({
|
||||
element: 'polyline',
|
||||
attr: {
|
||||
points: (x1 + ',' + y1 + midPt + x2 + ',' + y2),
|
||||
stroke: elem.getAttribute('stroke'),
|
||||
'stroke-width': elem.getAttribute('stroke-width'),
|
||||
fill: 'none',
|
||||
opacity: elem.getAttribute('opacity') || 1
|
||||
}
|
||||
});
|
||||
$.each(mtypes, function (i, pos) { // get any existing marker definitions
|
||||
const nam = 'marker-' + pos;
|
||||
const m = elem.getAttribute(nam);
|
||||
if (m) { pline.setAttribute(nam, elem.getAttribute(nam)); }
|
||||
});
|
||||
|
||||
const batchCmd = new S.BatchCommand();
|
||||
batchCmd.addSubCommand(new S.RemoveElementCommand(elem, elem.parentNode));
|
||||
batchCmd.addSubCommand(new S.InsertElementCommand(pline));
|
||||
|
||||
$(elem).after(pline).remove();
|
||||
svgCanvas.clearSelection();
|
||||
pline.id = id;
|
||||
svgCanvas.addToSelection([pline]);
|
||||
S.addCommandToHistory(batchCmd);
|
||||
return pline;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function setMarker () {
|
||||
const poslist = {start_marker: 'start', mid_marker: 'mid', end_marker: 'end'};
|
||||
const pos = poslist[this.id];
|
||||
const markerName = 'marker-' + pos;
|
||||
const el = selElems[0];
|
||||
const marker = getLinked(el, markerName);
|
||||
if (marker) { $(marker).remove(); }
|
||||
el.removeAttribute(markerName);
|
||||
let val = this.value;
|
||||
if (val === '') { val = '\\nomarker'; }
|
||||
if (val === '\\nomarker') {
|
||||
setIcon(pos, val);
|
||||
svgCanvas.call('changed', selElems);
|
||||
return;
|
||||
}
|
||||
// Set marker on element
|
||||
const id = markerPrefix + pos + '_' + el.id;
|
||||
addMarker(id, val);
|
||||
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
|
||||
if (el.tagName === 'line' && pos === 'mid') {
|
||||
convertline(el);
|
||||
}
|
||||
svgCanvas.call('changed', selElems);
|
||||
setIcon(pos, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the main system modifies an object. This routine changes
|
||||
* the associated markers to be the same color.
|
||||
* @param {Element} elem
|
||||
* @returns {void}
|
||||
*/
|
||||
function colorChanged (elem) {
|
||||
const color = elem.getAttribute('stroke');
|
||||
|
||||
$.each(mtypes, function (i, pos) {
|
||||
const marker = getLinked(elem, 'marker-' + pos);
|
||||
if (!marker) { return; }
|
||||
if (!marker.attributes.se_type) { return; } // not created by this extension
|
||||
const ch = marker.lastElementChild;
|
||||
if (!ch) { return; }
|
||||
const curfill = ch.getAttribute('fill');
|
||||
const curstroke = ch.getAttribute('stroke');
|
||||
if (curfill && curfill !== 'none') { ch.setAttribute('fill', color); }
|
||||
if (curstroke && curstroke !== 'none') { ch.setAttribute('stroke', color); }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the main system creates or modifies an object.
|
||||
* Its primary purpose is to create new markers for cloned objects.
|
||||
* @param {Element} el
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateReferences (el) {
|
||||
$.each(mtypes, function (i, pos) {
|
||||
const id = markerPrefix + pos + '_' + el.id;
|
||||
const markerName = 'marker-' + pos;
|
||||
const marker = getLinked(el, markerName);
|
||||
if (!marker || !marker.attributes.se_type) { return; } // not created by this extension
|
||||
const url = el.getAttribute(markerName);
|
||||
if (url) {
|
||||
const len = el.id.length;
|
||||
const linkid = url.substr(-len - 1, len);
|
||||
if (el.id !== linkid) {
|
||||
const val = $('#' + pos + '_marker').attr('value');
|
||||
addMarker(id, val);
|
||||
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
|
||||
if (el.tagName === 'line' && pos === 'mid') { el = convertline(el); }
|
||||
svgCanvas.call('changed', selElems);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// simulate a change event a text box that stores the current element's marker type
|
||||
/**
|
||||
* @param {"start"|"mid"|"end"} pos
|
||||
* @param {string} val
|
||||
* @returns {void}
|
||||
*/
|
||||
function triggerTextEntry (pos, val) {
|
||||
$('#' + pos + '_marker').val(val);
|
||||
$('#' + pos + '_marker').change();
|
||||
// const txtbox = $('#'+pos+'_marker');
|
||||
// if (val.substr(0,1)=='\\') {txtbox.hide();}
|
||||
// else {txtbox.show();}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"start"|"mid"|"end"} pos
|
||||
* @returns {Promise<void>} Resolves to `undefined`
|
||||
*/
|
||||
async function showTextPrompt (pos) {
|
||||
let def = $('#' + pos + '_marker').val();
|
||||
if (def.substr(0, 1) === '\\') { def = ''; }
|
||||
const txt = await $.prompt('Enter text for ' + pos + ' marker', def);
|
||||
if (txt) {
|
||||
triggerTextEntry(pos, txt);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
function setMarkerSet(obj) {
|
||||
const parts = this.id.split('_');
|
||||
const set = parts[2];
|
||||
switch (set) {
|
||||
case 'off':
|
||||
triggerTextEntry('start','\\nomarker');
|
||||
triggerTextEntry('mid','\\nomarker');
|
||||
triggerTextEntry('end','\\nomarker');
|
||||
break;
|
||||
case 'dimension':
|
||||
triggerTextEntry('start','\\leftarrow');
|
||||
triggerTextEntry('end','\\rightarrow');
|
||||
await showTextPrompt('mid');
|
||||
break;
|
||||
case 'label':
|
||||
triggerTextEntry('mid','\\nomarker');
|
||||
triggerTextEntry('end','\\rightarrow');
|
||||
await showTextPrompt('start');
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// callback function for a toolbar button click
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @returns {Promise<void>} Resolves to `undefined`
|
||||
*/
|
||||
async function setArrowFromButton (ev) {
|
||||
const parts = this.id.split('_');
|
||||
const pos = parts[1];
|
||||
let val = parts[2];
|
||||
if (parts[3]) { val += '_' + parts[3]; }
|
||||
|
||||
if (val !== 'textmarker') {
|
||||
triggerTextEntry(pos, '\\' + val);
|
||||
} else {
|
||||
await showTextPrompt(pos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} id
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTitle (id) {
|
||||
const {langList} = strings;
|
||||
const item = langList.find((itm) => {
|
||||
return itm.id === id;
|
||||
});
|
||||
return item ? item.title : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the toolbar button array from the marker definitions.
|
||||
* @returns {module:SVGEditor.Button[]}
|
||||
*/
|
||||
function buildButtonList () {
|
||||
const buttons = [];
|
||||
// const i = 0;
|
||||
/*
|
||||
buttons.push({
|
||||
id: idPrefix + 'markers_off',
|
||||
title: 'Turn off all markers',
|
||||
type: 'context',
|
||||
events: { click: setMarkerSet },
|
||||
panel: 'marker_panel'
|
||||
});
|
||||
buttons.push({
|
||||
id: idPrefix + 'markers_dimension',
|
||||
title: 'Dimension',
|
||||
type: 'context',
|
||||
events: { click: setMarkerSet },
|
||||
panel: 'marker_panel'
|
||||
});
|
||||
buttons.push({
|
||||
id: idPrefix + 'markers_label',
|
||||
title: 'Label',
|
||||
type: 'context',
|
||||
events: { click: setMarkerSet },
|
||||
panel: 'marker_panel'
|
||||
});
|
||||
*/
|
||||
$.each(mtypes, function (k, pos) {
|
||||
const listname = pos + '_marker_list';
|
||||
let def = true;
|
||||
Object.keys(markerTypes).forEach(function (id) {
|
||||
const title = getTitle(String(id));
|
||||
buttons.push({
|
||||
id: idPrefix + pos + '_' + id,
|
||||
svgicon: id,
|
||||
icon: svgEditor.curConfig.extIconsPath + 'markers-' + id + '.png',
|
||||
title,
|
||||
type: 'context',
|
||||
events: {click: setArrowFromButton},
|
||||
panel: 'marker_panel',
|
||||
list: listname,
|
||||
isDefault: def
|
||||
});
|
||||
def = false;
|
||||
});
|
||||
});
|
||||
return buttons;
|
||||
}
|
||||
|
||||
const contextTools = [
|
||||
{
|
||||
type: 'input',
|
||||
panel: 'marker_panel',
|
||||
id: 'start_marker',
|
||||
size: 3,
|
||||
events: {change: setMarker}
|
||||
}, {
|
||||
type: 'button-select',
|
||||
panel: 'marker_panel',
|
||||
id: 'start_marker_list',
|
||||
colnum: 3,
|
||||
events: {change: setArrowFromButton}
|
||||
}, {
|
||||
type: 'input',
|
||||
panel: 'marker_panel',
|
||||
id: 'mid_marker',
|
||||
defval: '',
|
||||
size: 3,
|
||||
events: {change: setMarker}
|
||||
}, {
|
||||
type: 'button-select',
|
||||
panel: 'marker_panel',
|
||||
id: 'mid_marker_list',
|
||||
colnum: 3,
|
||||
events: {change: setArrowFromButton}
|
||||
}, {
|
||||
type: 'input',
|
||||
panel: 'marker_panel',
|
||||
id: 'end_marker',
|
||||
size: 3,
|
||||
events: {change: setMarker}
|
||||
}, {
|
||||
type: 'button-select',
|
||||
panel: 'marker_panel',
|
||||
id: 'end_marker_list',
|
||||
colnum: 3,
|
||||
events: {change: setArrowFromButton}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'markers-icons.xml',
|
||||
callback () {
|
||||
$('#marker_panel').addClass('toolset').hide();
|
||||
},
|
||||
/* async */ addLangData ({importLocale, lang}) {
|
||||
return {data: strings.langList};
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
// Use this to update the current selected elements
|
||||
// console.log('selectChanged',opts);
|
||||
selElems = opts.elems;
|
||||
|
||||
const markerElems = ['line', 'path', 'polyline', 'polygon'];
|
||||
|
||||
let i = selElems.length;
|
||||
while (i--) {
|
||||
const elem = selElems[i];
|
||||
if (elem && markerElems.includes(elem.tagName)) {
|
||||
if (opts.selectedElement && !opts.multiselected) {
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
elementChanged (opts) {
|
||||
// console.log('elementChanged',opts);
|
||||
const elem = opts.elems[0];
|
||||
if (elem && (
|
||||
elem.getAttribute('marker-start') ||
|
||||
elem.getAttribute('marker-mid') ||
|
||||
elem.getAttribute('marker-end')
|
||||
)) {
|
||||
colorChanged(elem);
|
||||
updateReferences(elem);
|
||||
}
|
||||
// changing_flag = false; // Not apparently in use
|
||||
},
|
||||
buttons: buildButtonList(),
|
||||
context_tools: strings.contextTools.map((contextTool, i) => {
|
||||
return Object.assign(contextTools[i], contextTool);
|
||||
})
|
||||
};
|
||||
}
|
||||
};
|
||||
303
src/editor/extensions/ext-mathjax.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/* globals MathJax */
|
||||
/**
|
||||
* @file ext-mathjax.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2013 Jo Segaert
|
||||
*
|
||||
*/
|
||||
// Todo: Wait for Mathjax 3.0 to get ES Module/avoid global
|
||||
import {importScript} from '../external/dynamic-import-polyfill/importModule.js';
|
||||
|
||||
export default {
|
||||
name: 'mathjax',
|
||||
async init ({$, importLocale}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
|
||||
// Configuration of the MathJax extention.
|
||||
|
||||
// This will be added to the head tag before MathJax is loaded.
|
||||
const /* mathjaxConfiguration = `<script type="text/x-mathjax-config">
|
||||
MathJax.Hub.Config({
|
||||
extensions: ['tex2jax.js'],
|
||||
jax: ['input/TeX', 'output/SVG'],
|
||||
showProcessingMessages: true,
|
||||
showMathMenu: false,
|
||||
showMathMenuMSIE: false,
|
||||
errorSettings: {
|
||||
message: ['[Math Processing Error]'],
|
||||
style: {color: '#CC0000', 'font-style': 'italic'}
|
||||
},
|
||||
elements: [],
|
||||
tex2jax: {
|
||||
ignoreClass: 'tex2jax_ignore2', processClass: 'tex2jax_process2',
|
||||
},
|
||||
TeX: {
|
||||
extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js']
|
||||
},
|
||||
SVG: {
|
||||
}
|
||||
});
|
||||
</script>`, */
|
||||
// mathjaxSrc = 'http://cdn.mathjax.org/mathjax/latest/MathJax.js',
|
||||
// Had been on https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_SVG.js
|
||||
// Obtained Text-AMS-MML_SVG.js from https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.3/config/TeX-AMS-MML_SVG.js
|
||||
mathjaxSrcSecure = 'mathjax/MathJax.min.js?config=TeX-AMS-MML_SVG.js',
|
||||
{uiStrings} = svgEditor;
|
||||
let
|
||||
math,
|
||||
locationX,
|
||||
locationY,
|
||||
mathjaxLoaded = false;
|
||||
|
||||
// TODO: Implement language support. Move these uiStrings to the locale files and
|
||||
// the code to the langReady callback. Also i18nize alert and HTML below
|
||||
$.extend(uiStrings, {
|
||||
mathjax: {
|
||||
embed_svg: 'Save as mathematics',
|
||||
embed_mathml: 'Save as figure',
|
||||
svg_save_warning: 'The math will be transformed into a figure is manipulatable like everything else. You will not be able to manipulate the TeX-code anymore. ',
|
||||
mathml_save_warning: 'Advised. The math will be saved as a figure.',
|
||||
title: 'Mathematics code editor'
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function saveMath () {
|
||||
const code = $('#mathjax_code_textarea').val();
|
||||
// displaystyle to force MathJax NOT to use the inline style. Because it is
|
||||
// less fancy!
|
||||
MathJax.Hub.queue.Push(['Text', math, '\\displaystyle{' + code + '}']);
|
||||
|
||||
/*
|
||||
* The MathJax library doesn't want to bloat your webpage so it creates
|
||||
* every symbol (glymph) you need only once. These are saved in a `<svg>` on
|
||||
* the top of your html document, just under the body tag. Each glymph has
|
||||
* its unique id and is saved as a `<path>` in the `<defs>` tag of the `<svg>`
|
||||
*
|
||||
* Then when the symbols are needed in the rest of your html document they
|
||||
* are refferd to by a `<use>` tag.
|
||||
* Because of bug 1076 we can't just grab the defs tag on the top and add it
|
||||
* to your formula's `<svg>` and copy the lot. So we have to replace each
|
||||
* `<use>` tag by its `<path>`.
|
||||
*/
|
||||
MathJax.Hub.queue.Push(
|
||||
function () {
|
||||
const mathjaxMath = $('.MathJax_SVG');
|
||||
const svg = $(mathjaxMath.html());
|
||||
svg.find('use').each(function () {
|
||||
// TODO: find a less pragmatic and more elegant solution to this.
|
||||
const id = $(this).attr('href')
|
||||
? $(this).attr('href').slice(1) // Works in Chrome.
|
||||
: $(this).attr('xlink:href').slice(1); // Works in Firefox.
|
||||
const glymph = $('#' + id).clone().removeAttr('id');
|
||||
const x = $(this).attr('x');
|
||||
const y = $(this).attr('y');
|
||||
const transform = $(this).attr('transform');
|
||||
if (transform && (x || y)) {
|
||||
glymph.attr('transform', transform + ' translate(' + x + ',' + y + ')');
|
||||
} else if (transform) {
|
||||
glymph.attr('transform', transform);
|
||||
} else if (x || y) {
|
||||
glymph.attr('transform', 'translate(' + x + ',' + y + ')');
|
||||
}
|
||||
$(this).replaceWith(glymph);
|
||||
});
|
||||
// Remove the style tag because it interferes with SVG-Edit.
|
||||
svg.removeAttr('style');
|
||||
svg.attr('xmlns', 'http://www.w3.org/2000/svg');
|
||||
svgCanvas.importSvgString($('<div>').append(svg.clone()).html(), true);
|
||||
svgCanvas.ungroupSelectedElement();
|
||||
// TODO: To undo the adding of the Formula you now have to undo twice.
|
||||
// This should only be once!
|
||||
svgCanvas.moveSelectedElements(locationX, locationY, true);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const buttons = [{
|
||||
id: 'tool_mathjax',
|
||||
type: 'mode',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'mathjax.png',
|
||||
events: {
|
||||
async click () {
|
||||
// Set the mode.
|
||||
svgCanvas.setMode('mathjax');
|
||||
|
||||
// Only load Mathjax when needed, we don't want to strain Svg-Edit any more.
|
||||
// From this point on it is very probable that it will be needed, so load it.
|
||||
if (mathjaxLoaded === false) {
|
||||
$(
|
||||
'<div id="mathjax">' +
|
||||
'<!-- Here is where MathJax creates the math -->' +
|
||||
'<div id="mathjax_creator" class="tex2jax_process" style="display:none">' +
|
||||
'$${}$$' +
|
||||
'</div>' +
|
||||
'<div id="mathjax_overlay"></div>' +
|
||||
'<div id="mathjax_container">' +
|
||||
'<div id="tool_mathjax_back" class="toolbar_button">' +
|
||||
'<button id="tool_mathjax_save">OK</button>' +
|
||||
'<button id="tool_mathjax_cancel">Cancel</button>' +
|
||||
'</div>' +
|
||||
'<fieldset>' +
|
||||
'<legend id="mathjax_legend">Mathematics Editor</legend>' +
|
||||
'<label>' +
|
||||
'<span id="mathjax_explication">Please type your mathematics in ' +
|
||||
'<a href="https://en.wikipedia.org/wiki/Help:Displaying_a_formula" target="_blank">TeX</a> code.</span></label>' +
|
||||
'<textarea id="mathjax_code_textarea" spellcheck="false"></textarea>' +
|
||||
'</fieldset>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
).insertAfter('#svg_prefs').hide();
|
||||
|
||||
// Make the MathEditor draggable.
|
||||
$('#mathjax_container').draggable({
|
||||
cancel: 'button,fieldset',
|
||||
containment: 'window'
|
||||
});
|
||||
|
||||
// Add functionality and picture to cancel button.
|
||||
$('#tool_mathjax_cancel').prepend($.getSvgIcon('cancel', true))
|
||||
.on('click touched', function () {
|
||||
$('#mathjax').hide();
|
||||
});
|
||||
|
||||
// Add functionality and picture to the save button.
|
||||
$('#tool_mathjax_save').prepend($.getSvgIcon('ok', true))
|
||||
.on('click touched', function () {
|
||||
saveMath();
|
||||
$('#mathjax').hide();
|
||||
});
|
||||
|
||||
// MathJax preprocessing has to ignore most of the page.
|
||||
$('body').addClass('tex2jax_ignore');
|
||||
|
||||
// Now get (and run) the MathJax Library.
|
||||
// Todo: insert script with modules once widely supported
|
||||
// and if MathJax (and its `TeX-AMS-MML_SVG.js` dependency) ends up
|
||||
// providing an ES6 module export: https://github.com/mathjax/MathJax/issues/1998
|
||||
/*
|
||||
const modularVersion = !('svgEditor' in window) ||
|
||||
!window.svgEditor ||
|
||||
window.svgEditor.modules !== false;
|
||||
// Add as second argument to `importScript`
|
||||
{
|
||||
type: modularVersion
|
||||
? 'module' // Make this the default when widely supported
|
||||
: 'text/javascript'
|
||||
}
|
||||
// If only using modules, just use this:
|
||||
const {default: MathJax} = await importModule( // or `import()` when widely supported
|
||||
svgEditor.curConfig.extIconsPath + mathjaxSrcSecure
|
||||
);
|
||||
*/
|
||||
// We use `extIconsPath` here for now as it does not vary with
|
||||
// the modular type as does `extPath`
|
||||
try {
|
||||
await importScript(svgEditor.curConfig.extIconsPath + mathjaxSrcSecure);
|
||||
// When MathJax is loaded get the div where the math will be rendered.
|
||||
MathJax.Hub.queue.Push(function () {
|
||||
math = MathJax.Hub.getAllJax('#mathjax_creator')[0];
|
||||
console.log(math); // eslint-disable-line no-console
|
||||
mathjaxLoaded = true;
|
||||
console.log('MathJax Loaded'); // eslint-disable-line no-console
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Failed loading MathJax.'); // eslint-disable-line no-console
|
||||
$.alert('Failed loading MathJax. You will not be able to change the mathematics.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'mathjax-icons.xml',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
|
||||
mouseDown () {
|
||||
if (svgCanvas.getMode() === 'mathjax') {
|
||||
return {started: true};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
mouseUp (opts) {
|
||||
if (svgCanvas.getMode() === 'mathjax') {
|
||||
// Get the coordinates from your mouse.
|
||||
const zoom = svgCanvas.getZoom();
|
||||
// Get the actual coordinate by dividing by the zoom value
|
||||
locationX = opts.mouse_x / zoom;
|
||||
locationY = opts.mouse_y / zoom;
|
||||
|
||||
$('#mathjax').show();
|
||||
return {started: false}; // Otherwise the last selected object dissapears.
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
callback () {
|
||||
$('<style>').text(
|
||||
'#mathjax fieldset{' +
|
||||
'padding: 5px;' +
|
||||
'margin: 5px;' +
|
||||
'border: 1px solid #DDD;' +
|
||||
'}' +
|
||||
'#mathjax label{' +
|
||||
'display: block;' +
|
||||
'margin: .5em;' +
|
||||
'}' +
|
||||
'#mathjax legend {' +
|
||||
'max-width:195px;' +
|
||||
'}' +
|
||||
'#mathjax_overlay {' +
|
||||
'position: absolute;' +
|
||||
'top: 0;' +
|
||||
'left: 0;' +
|
||||
'right: 0;' +
|
||||
'bottom: 0;' +
|
||||
'background-color: black;' +
|
||||
'opacity: 0.6;' +
|
||||
'z-index: 20000;' +
|
||||
'}' +
|
||||
'#mathjax_container {' +
|
||||
'position: absolute;' +
|
||||
'top: 50px;' +
|
||||
'padding: 10px;' +
|
||||
'background-color: #B0B0B0;' +
|
||||
'border: 1px outset #777;' +
|
||||
'opacity: 1.0;' +
|
||||
'font-family: Verdana, Helvetica, sans-serif;' +
|
||||
'font-size: .8em;' +
|
||||
'z-index: 20001;' +
|
||||
'}' +
|
||||
'#tool_mathjax_back {' +
|
||||
'margin-left: 1em;' +
|
||||
'overflow: auto;' +
|
||||
'}' +
|
||||
'#mathjax_legend{' +
|
||||
'font-weight: bold;' +
|
||||
'font-size:1.1em;' +
|
||||
'}' +
|
||||
'#mathjax_code_textarea {\\n' +
|
||||
'margin: 5px .7em;' +
|
||||
'overflow: hidden;' +
|
||||
'width: 416px;' +
|
||||
'display: block;' +
|
||||
'height: 100px;' +
|
||||
'}'
|
||||
).appendTo('head');
|
||||
|
||||
// Add the MathJax configuration.
|
||||
// $(mathjaxConfiguration).appendTo('head');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
151
src/editor/extensions/ext-overview_window.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* @file ext-overview_window.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2013 James Sacksteder
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'overview_window',
|
||||
init ({$, isChrome, isIE}) {
|
||||
const overviewWindowGlobals = {};
|
||||
// Disabled in Chrome 48-, see https://github.com/SVG-Edit/svgedit/issues/26 and
|
||||
// https://code.google.com/p/chromium/issues/detail?id=565120.
|
||||
if (isChrome()) {
|
||||
const verIndex = navigator.userAgent.indexOf('Chrome/') + 7;
|
||||
const chromeVersion = Number.parseInt(navigator.userAgent.substring(verIndex));
|
||||
if (chromeVersion < 49) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Define and insert the base html element.
|
||||
const propsWindowHtml =
|
||||
'<div id="overview_window_content_pane" style="width:100%; word-wrap:break-word; display:inline-block; margin-top:20px;">' +
|
||||
'<div id="overview_window_content" style="position:relative; left:12px; top:0px;">' +
|
||||
'<div style="background-color:#A0A0A0; display:inline-block; overflow:visible;">' +
|
||||
'<svg id="overviewMiniView" width="150" height="100" x="0" y="0" viewBox="0 0 4800 3600" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">' +
|
||||
'<use x="0" y="0" xlink:href="#svgroot"> </use>' +
|
||||
'</svg>' +
|
||||
'<div id="overview_window_view_box" style="min-width:50px; min-height:50px; position:absolute; top:30px; left:30px; z-index:5; background-color:rgba(255,0,102,0.3);">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
$('#sidepanels').append(propsWindowHtml);
|
||||
|
||||
// Define dynamic animation of the view box.
|
||||
const updateViewBox = function () {
|
||||
const portHeight = Number.parseFloat($('#workarea').css('height'));
|
||||
const portWidth = Number.parseFloat($('#workarea').css('width'));
|
||||
const portX = $('#workarea').scrollLeft();
|
||||
const portY = $('#workarea').scrollTop();
|
||||
const windowWidth = Number.parseFloat($('#svgcanvas').css('width'));
|
||||
const windowHeight = Number.parseFloat($('#svgcanvas').css('height'));
|
||||
const overviewWidth = $('#overviewMiniView').attr('width');
|
||||
const overviewHeight = $('#overviewMiniView').attr('height');
|
||||
|
||||
const viewBoxX = portX / windowWidth * overviewWidth;
|
||||
const viewBoxY = portY / windowHeight * overviewHeight;
|
||||
const viewBoxWidth = portWidth / windowWidth * overviewWidth;
|
||||
const viewBoxHeight = portHeight / windowHeight * overviewHeight;
|
||||
|
||||
$('#overview_window_view_box').css('min-width', viewBoxWidth + 'px');
|
||||
$('#overview_window_view_box').css('min-height', viewBoxHeight + 'px');
|
||||
$('#overview_window_view_box').css('top', viewBoxY + 'px');
|
||||
$('#overview_window_view_box').css('left', viewBoxX + 'px');
|
||||
};
|
||||
$('#workarea').scroll(function () {
|
||||
if (!(overviewWindowGlobals.viewBoxDragging)) {
|
||||
updateViewBox();
|
||||
}
|
||||
});
|
||||
$('#workarea').resize(updateViewBox);
|
||||
updateViewBox();
|
||||
|
||||
// Compensate for changes in zoom and canvas size.
|
||||
const updateViewDimensions = function () {
|
||||
const viewWidth = $('#svgroot').attr('width');
|
||||
const viewHeight = $('#svgroot').attr('height');
|
||||
|
||||
let viewX = 640;
|
||||
let viewY = 480;
|
||||
if (isIE()) {
|
||||
// This has only been tested with Firefox 10 and IE 9 (without chrome frame).
|
||||
// I am not sure if if is Firefox or IE that is being non compliant here.
|
||||
// Either way the one that is noncompliant may become more compliant later.
|
||||
// TAG:HACK
|
||||
// TAG:VERSION_DEPENDENT
|
||||
// TAG:BROWSER_SNIFFING
|
||||
viewX = 0;
|
||||
viewY = 0;
|
||||
}
|
||||
|
||||
const svgWidthOld = $('#overviewMiniView').attr('width');
|
||||
const svgHeightNew = viewHeight / viewWidth * svgWidthOld;
|
||||
$('#overviewMiniView').attr('viewBox', viewX + ' ' + viewY + ' ' + viewWidth + ' ' + viewHeight);
|
||||
$('#overviewMiniView').attr('height', svgHeightNew);
|
||||
updateViewBox();
|
||||
};
|
||||
updateViewDimensions();
|
||||
|
||||
// Set up the overview window as a controller for the view port.
|
||||
overviewWindowGlobals.viewBoxDragging = false;
|
||||
const updateViewPortFromViewBox = function () {
|
||||
const windowWidth = Number.parseFloat($('#svgcanvas').css('width'));
|
||||
const windowHeight = Number.parseFloat($('#svgcanvas').css('height'));
|
||||
const overviewWidth = $('#overviewMiniView').attr('width');
|
||||
const overviewHeight = $('#overviewMiniView').attr('height');
|
||||
const viewBoxX = Number.parseFloat($('#overview_window_view_box').css('left'));
|
||||
const viewBoxY = Number.parseFloat($('#overview_window_view_box').css('top'));
|
||||
|
||||
const portX = viewBoxX / overviewWidth * windowWidth;
|
||||
const portY = viewBoxY / overviewHeight * windowHeight;
|
||||
|
||||
$('#workarea').scrollLeft(portX);
|
||||
$('#workarea').scrollTop(portY);
|
||||
};
|
||||
$('#overview_window_view_box').draggable({
|
||||
containment: 'parent',
|
||||
drag: updateViewPortFromViewBox,
|
||||
start () { overviewWindowGlobals.viewBoxDragging = true; },
|
||||
stop () { overviewWindowGlobals.viewBoxDragging = false; }
|
||||
});
|
||||
$('#overviewMiniView').click(function (evt) {
|
||||
// Firefox doesn't support evt.offsetX and evt.offsetY.
|
||||
const mouseX = (evt.offsetX || evt.originalEvent.layerX);
|
||||
const mouseY = (evt.offsetY || evt.originalEvent.layerY);
|
||||
const overviewWidth = $('#overviewMiniView').attr('width');
|
||||
const overviewHeight = $('#overviewMiniView').attr('height');
|
||||
const viewBoxWidth = Number.parseFloat($('#overview_window_view_box').css('min-width'));
|
||||
const viewBoxHeight = Number.parseFloat($('#overview_window_view_box').css('min-height'));
|
||||
|
||||
let viewBoxX = mouseX - 0.5 * viewBoxWidth;
|
||||
let viewBoxY = mouseY - 0.5 * viewBoxHeight;
|
||||
// deal with constraints
|
||||
if (viewBoxX < 0) {
|
||||
viewBoxX = 0;
|
||||
}
|
||||
if (viewBoxY < 0) {
|
||||
viewBoxY = 0;
|
||||
}
|
||||
if (viewBoxX + viewBoxWidth > overviewWidth) {
|
||||
viewBoxX = overviewWidth - viewBoxWidth;
|
||||
}
|
||||
if (viewBoxY + viewBoxHeight > overviewHeight) {
|
||||
viewBoxY = overviewHeight - viewBoxHeight;
|
||||
}
|
||||
|
||||
$('#overview_window_view_box').css('top', viewBoxY + 'px');
|
||||
$('#overview_window_view_box').css('left', viewBoxX + 'px');
|
||||
updateViewPortFromViewBox();
|
||||
});
|
||||
|
||||
return {
|
||||
name: 'overview window',
|
||||
canvasUpdated: updateViewDimensions,
|
||||
workareaResized: updateViewBox
|
||||
};
|
||||
}
|
||||
};
|
||||
53
src/editor/extensions/ext-panning.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file ext-panning.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2013 Luis Aguirre
|
||||
*
|
||||
*/
|
||||
/*
|
||||
This is a very basic SVG-Edit extension to let tablet/mobile devices pan without problem
|
||||
*/
|
||||
export default {
|
||||
name: 'panning',
|
||||
async init ({importLocale}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const buttons = [{
|
||||
id: 'ext-panning',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'panning.png',
|
||||
type: 'mode',
|
||||
events: {
|
||||
click () {
|
||||
svgCanvas.setMode('ext-panning');
|
||||
}
|
||||
}
|
||||
}];
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'ext-panning.xml',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
mouseDown () {
|
||||
if (svgCanvas.getMode() === 'ext-panning') {
|
||||
svgEditor.setPanning(true);
|
||||
return {started: true};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
mouseUp () {
|
||||
if (svgCanvas.getMode() === 'ext-panning') {
|
||||
svgEditor.setPanning(false);
|
||||
return {
|
||||
keep: false,
|
||||
element: null
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
6
src/editor/extensions/ext-panning.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="ext-panning">
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||
<path d="m8.00038,150.84583l51.60005,-51.78485l0,25.89205l26.28711,0l35.45559,-0.20444l-0.72941,-24.34613l0.93304,-37.61812l-25.79949,0l51.5997,-51.78508l51.60047,51.78508l-25.80024,0l0,33.87256l1.13677,26.21891l21.45996,2.07722l39.3497,0l0,-25.89205l51.60043,51.78485l-51.60043,51.78563l0,-25.89281l-38.41666,-0.93639l-20.52692,0.20445l-3.00285,42.13754l0,20.76308l25.80024,0l-51.60047,51.78561l-51.5997,-51.78561l25.79949,0l0,-20.76308l0.72941,-41.20115l-41.98688,-0.20445l-20.68886,0l0,25.89281l-51.60005,-51.78563z" fill="#b2b2b2" id="svg_1" stroke="#000000" stroke-width="10"/> </svg>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 789 B |
30
src/editor/extensions/ext-php_savefile.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// TODO: Might add support for "exportImage" custom
|
||||
// handler as in "ext-server_opensave.js" (and in savefile.php)
|
||||
|
||||
export default {
|
||||
name: 'php_savefile',
|
||||
init ({$}) {
|
||||
const svgEditor = this;
|
||||
const {
|
||||
curConfig: {extPath},
|
||||
canvas: svgCanvas
|
||||
} = svgEditor;
|
||||
/**
|
||||
* Get file name out of SVGEdit document title.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getFileNameFromTitle () {
|
||||
const title = svgCanvas.getDocumentTitle();
|
||||
return title.trim();
|
||||
}
|
||||
const saveSvgAction = extPath + 'savefile.php';
|
||||
svgEditor.setCustomHandlers({
|
||||
save (win, data) {
|
||||
const svg = '<?xml version="1.0" encoding="UTF-8"?>\n' + data,
|
||||
filename = getFileNameFromTitle();
|
||||
|
||||
$.post(saveSvgAction, {output_svg: svg, filename});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
538
src/editor/extensions/ext-placemark.js
Normal file
@@ -0,0 +1,538 @@
|
||||
/**
|
||||
* @file ext-placemark.js
|
||||
*
|
||||
*
|
||||
* @copyright 2010 CloudCanvas, Inc. All rights reserved
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'placemark',
|
||||
async init (S) {
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const addElem = svgCanvas.addSVGElementFromJson;
|
||||
const {$, importLocale} = S; // {svgcontent},
|
||||
let
|
||||
selElems,
|
||||
// editingitex = false,
|
||||
// svgdoc = S.svgroot.parentNode.ownerDocument,
|
||||
started,
|
||||
newPM;
|
||||
// edg = 0,
|
||||
// newFOG, newFOGParent, newDef, newImageName, newMaskID,
|
||||
// undoCommand = 'Not image',
|
||||
// modeChangeG, ccZoom, wEl, hEl, wOffset, hOffset, ccRgbEl, brushW, brushH;
|
||||
const strings = await importLocale();
|
||||
const markerTypes = {
|
||||
nomarker: {},
|
||||
forwardslash:
|
||||
{element: 'path', attr: {d: 'M30,100 L70,0'}},
|
||||
reverseslash:
|
||||
{element: 'path', attr: {d: 'M30,0 L70,100'}},
|
||||
verticalslash:
|
||||
{element: 'path', attr: {d: 'M50,0 L50,100'}},
|
||||
xmark:
|
||||
{element: 'path', attr: {d: 'M20,80 L80,20 M80,80 L20,20'}},
|
||||
leftarrow:
|
||||
{element: 'path', attr: {d: 'M0,50 L100,90 L70,50 L100,10 Z'}},
|
||||
rightarrow:
|
||||
{element: 'path', attr: {d: 'M100,50 L0,90 L30,50 L0,10 Z'}},
|
||||
box:
|
||||
{element: 'path', attr: {d: 'M20,20 L20,80 L80,80 L80,20 Z'}},
|
||||
star:
|
||||
{element: 'path', attr: {d: 'M10,30 L90,30 L20,90 L50,10 L80,90 Z'}},
|
||||
mcircle:
|
||||
{element: 'circle', attr: {r: 30, cx: 50, cy: 50}},
|
||||
triangle:
|
||||
{element: 'path', attr: {d: 'M10,80 L50,20 L80,80 Z'}}
|
||||
};
|
||||
|
||||
// duplicate shapes to support unfilled (open) marker types with an _o suffix
|
||||
['leftarrow', 'rightarrow', 'box', 'star', 'mcircle', 'triangle'].forEach((v) => {
|
||||
markerTypes[v + '_o'] = markerTypes[v];
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function showPanel (on) {
|
||||
$('#placemark_panel').toggle(on);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} elem - A graphic element will have an attribute like marker-start
|
||||
* @param {"marker-start"|"marker-mid"|"marker-end"} attr
|
||||
* @returns {Element} The marker element that is linked to the graphic element
|
||||
*/
|
||||
function getLinked (elem, attr) {
|
||||
if (!elem) { return null; }
|
||||
const str = elem.getAttribute(attr);
|
||||
if (!str) { return null; }
|
||||
|
||||
// const m = str.match(/\(#(?<id>.+)\)/);
|
||||
// if (!m || !m.groups.id) {
|
||||
const m = str.match(/\(#(.*)\)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
return svgCanvas.getElem(m[1]);
|
||||
// return svgCanvas.getElem(m.groups.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when text is changed.
|
||||
* @param {string} txt
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateText (txt) {
|
||||
const items = txt.split(';');
|
||||
selElems.forEach((elem) => {
|
||||
if (elem && elem.getAttribute('class').includes('placemark')) {
|
||||
$(elem).children().each((_, i) => {
|
||||
const [, , type, n] = i.id.split('_');
|
||||
if (type === 'txt') {
|
||||
$(i).text(items[n]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Called when font is changed.
|
||||
* @param {string} font
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateFont (font) {
|
||||
font = font.split(' ');
|
||||
const fontSize = Number.parseInt(font.pop());
|
||||
font = font.join(' ');
|
||||
selElems.forEach((elem) => {
|
||||
if (elem && elem.getAttribute('class').includes('placemark')) {
|
||||
$(elem).children().each((_, i) => {
|
||||
const [, , type] = i.id.split('_');
|
||||
if (type === 'txt') {
|
||||
$(i).attr({'font-family': font, 'font-size': fontSize});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {""|"\\nomarker"|"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"textmarker_top"|"textmarker_bottom"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} val
|
||||
* @returns {SVGMarkerElement}
|
||||
*/
|
||||
function addMarker (id, val) {
|
||||
let marker = svgCanvas.getElem(id);
|
||||
if (marker) { return undefined; }
|
||||
// console.log(id);
|
||||
if (val === '' || val === 'nomarker') { return undefined; }
|
||||
const color = svgCanvas.getColor('stroke');
|
||||
// NOTE: Safari didn't like a negative value in viewBox
|
||||
// so we use a standardized 0 0 100 100
|
||||
// with 50 50 being mapped to the marker position
|
||||
const scale = 2;// parseFloat($('#marker_size').val());
|
||||
const strokeWidth = 10;
|
||||
let refX = 50;
|
||||
const refY = 50;
|
||||
const viewBox = '0 0 100 100';
|
||||
const markerWidth = 5 * scale;
|
||||
const markerHeight = 5 * scale;
|
||||
const seType = val;
|
||||
|
||||
if (!markerTypes[seType]) { return undefined; } // an unknown type!
|
||||
// positional markers(arrows) at end of line
|
||||
if (seType.includes('left')) refX = 0;
|
||||
if (seType.includes('right')) refX = 100;
|
||||
|
||||
// create a generic marker
|
||||
marker = addElem({
|
||||
element: 'marker',
|
||||
attr: {
|
||||
id,
|
||||
markerUnits: 'strokeWidth',
|
||||
orient: 'auto',
|
||||
style: 'pointer-events:none',
|
||||
class: seType
|
||||
}
|
||||
});
|
||||
|
||||
const mel = addElem(markerTypes[seType]);
|
||||
const fillcolor = (seType.substr(-2) === '_o')
|
||||
? 'none'
|
||||
: color;
|
||||
|
||||
mel.setAttribute('fill', fillcolor);
|
||||
mel.setAttribute('stroke', color);
|
||||
mel.setAttribute('stroke-width', strokeWidth);
|
||||
marker.append(mel);
|
||||
|
||||
marker.setAttribute('viewBox', viewBox);
|
||||
marker.setAttribute('markerWidth', markerWidth);
|
||||
marker.setAttribute('markerHeight', markerHeight);
|
||||
marker.setAttribute('refX', refX);
|
||||
marker.setAttribute('refY', refY);
|
||||
svgCanvas.findDefs().append(marker);
|
||||
|
||||
return marker;
|
||||
}
|
||||
/**
|
||||
* @param {Element} el
|
||||
* @param {string} val
|
||||
* @returns {void}
|
||||
*/
|
||||
function setMarker (el, val) {
|
||||
const markerName = 'marker-start';
|
||||
const marker = getLinked(el, markerName);
|
||||
if (marker) { $(marker).remove(); }
|
||||
el.removeAttribute(markerName);
|
||||
if (val === 'nomarker') {
|
||||
svgCanvas.call('changed', [el]);
|
||||
return;
|
||||
}
|
||||
// Set marker on element
|
||||
const id = 'placemark_marker_' + el.id;
|
||||
addMarker(id, val);
|
||||
el.setAttribute(markerName, 'url(#' + id + ')');
|
||||
svgCanvas.call('changed', [el]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the main system modifies an object. This routine changes
|
||||
* the associated markers to be the same color.
|
||||
* @param {Element} el
|
||||
* @returns {void}
|
||||
*/
|
||||
function colorChanged (el) {
|
||||
const color = el.getAttribute('stroke');
|
||||
const marker = getLinked(el, 'marker-start');
|
||||
// console.log(marker);
|
||||
if (!marker) { return; }
|
||||
if (!marker.attributes.class) { return; } // not created by this extension
|
||||
const ch = marker.lastElementChild;
|
||||
if (!ch) { return; }
|
||||
const curfill = ch.getAttribute('fill');
|
||||
const curstroke = ch.getAttribute('stroke');
|
||||
if (curfill && curfill !== 'none') { ch.setAttribute('fill', color); }
|
||||
if (curstroke && curstroke !== 'none') { ch.setAttribute('stroke', color); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the main system creates or modifies an object.
|
||||
* Its primary purpose is to create new markers for cloned objects.
|
||||
* @param {Element} el
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateReferences (el) {
|
||||
const id = 'placemark_marker_' + el.id;
|
||||
const markerName = 'marker-start';
|
||||
const marker = getLinked(el, markerName);
|
||||
if (!marker || !marker.attributes.class) { return; } // not created by this extension
|
||||
const url = el.getAttribute(markerName);
|
||||
if (url) {
|
||||
const len = el.id.length;
|
||||
const linkid = url.substr(-len - 1, len);
|
||||
if (el.id !== linkid) {
|
||||
const val = $('#placemark_marker').attr('value') || 'leftarrow';
|
||||
addMarker(id, val);
|
||||
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
|
||||
svgCanvas.call('changed', selElems);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @returns {void}
|
||||
*/
|
||||
function setArrowFromButton (ev) {
|
||||
const parts = this.id.split('_');
|
||||
let val = parts[2];
|
||||
if (parts[3]) { val += '_' + parts[3]; }
|
||||
$('#placemark_marker').attr('value', val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} id
|
||||
* @returns {string}
|
||||
*/
|
||||
function getTitle (id) {
|
||||
const {langList} = strings;
|
||||
const item = langList.find((itm) => {
|
||||
return itm.id === id;
|
||||
});
|
||||
return item ? item.title : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the toolbar button array from the marker definitions.
|
||||
* @param {module:SVGEditor.Button[]} buttons
|
||||
* @returns {module:SVGEditor.Button[]}
|
||||
*/
|
||||
function addMarkerButtons (buttons) {
|
||||
Object.keys(markerTypes).forEach(function (id) {
|
||||
const title = getTitle(String(id));
|
||||
buttons.push({
|
||||
id: 'placemark_marker_' + id,
|
||||
svgicon: id,
|
||||
icon: svgEditor.curConfig.extIconsPath + 'markers-' + id + '.png',
|
||||
title,
|
||||
type: 'context',
|
||||
events: {click: setArrowFromButton},
|
||||
panel: 'placemark_panel',
|
||||
list: 'placemark_marker',
|
||||
isDefault: id === 'leftarrow'
|
||||
});
|
||||
});
|
||||
return buttons;
|
||||
}
|
||||
|
||||
const buttons = [{
|
||||
id: 'tool_placemark',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'placemark.png',
|
||||
type: 'mode',
|
||||
position: 12,
|
||||
events: {
|
||||
click () {
|
||||
showPanel(true);
|
||||
svgCanvas.setMode('placemark');
|
||||
}
|
||||
}
|
||||
}];
|
||||
const contextTools = [
|
||||
{
|
||||
type: 'button-select',
|
||||
panel: 'placemark_panel',
|
||||
id: 'placemark_marker',
|
||||
colnum: 3,
|
||||
events: {change: setArrowFromButton}
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
panel: 'placemark_panel',
|
||||
id: 'placemarkText',
|
||||
size: 20,
|
||||
defval: '',
|
||||
events: {
|
||||
change () {
|
||||
updateText(this.value);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
panel: 'placemark_panel',
|
||||
id: 'placemarkFont',
|
||||
size: 7,
|
||||
defval: 'Arial 10',
|
||||
events: {
|
||||
change () {
|
||||
updateFont(this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'placemark-icons.xml',
|
||||
buttons: addMarkerButtons(strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
})),
|
||||
context_tools: strings.contextTools.map((contextTool, i) => {
|
||||
return Object.assign(contextTools[i], contextTool);
|
||||
}),
|
||||
callback () {
|
||||
$('#placemark_panel').hide();
|
||||
// const endChanges = function(){};
|
||||
},
|
||||
mouseDown (opts) {
|
||||
// const rgb = svgCanvas.getColor('fill');
|
||||
const sRgb = svgCanvas.getColor('stroke');
|
||||
const sWidth = svgCanvas.getStrokeWidth();
|
||||
|
||||
if (svgCanvas.getMode() === 'placemark') {
|
||||
started = true;
|
||||
const id = svgCanvas.getNextId();
|
||||
const items = $('#placemarkText').val().split(';');
|
||||
let font = $('#placemarkFont').val().split(' ');
|
||||
const fontSize = Number.parseInt(font.pop());
|
||||
font = font.join(' ');
|
||||
const x0 = opts.start_x + 10, y0 = opts.start_y + 10;
|
||||
let maxlen = 0;
|
||||
const children = [{
|
||||
element: 'line',
|
||||
attr: {
|
||||
id: id + '_pline_0',
|
||||
fill: 'none',
|
||||
stroke: sRgb,
|
||||
'stroke-width': sWidth,
|
||||
'stroke-linecap': 'round',
|
||||
x1: opts.start_x,
|
||||
y1: opts.start_y,
|
||||
x2: x0,
|
||||
y2: y0
|
||||
}
|
||||
}];
|
||||
items.forEach((i, n) => {
|
||||
maxlen = Math.max(maxlen, i.length);
|
||||
children.push({
|
||||
element: 'line',
|
||||
attr: {
|
||||
id: id + '_tline_' + n,
|
||||
fill: 'none',
|
||||
stroke: sRgb,
|
||||
'stroke-width': sWidth,
|
||||
'stroke-linecap': 'round',
|
||||
x1: x0,
|
||||
y1: y0 + (fontSize + 6) * n,
|
||||
x2: x0 + i.length * fontSize * 0.5 + fontSize,
|
||||
y2: y0 + (fontSize + 6) * n
|
||||
}
|
||||
});
|
||||
children.push({
|
||||
element: 'text',
|
||||
attr: {
|
||||
id: id + '_txt_' + n,
|
||||
fill: sRgb,
|
||||
stroke: 'none',
|
||||
'stroke-width': 0,
|
||||
x: x0 + 3,
|
||||
y: y0 - 3 + (fontSize + 6) * n,
|
||||
'font-family': font,
|
||||
'font-size': fontSize,
|
||||
'text-anchor': 'start'
|
||||
},
|
||||
children: [i]
|
||||
});
|
||||
});
|
||||
if (items.length > 0) {
|
||||
children.push({
|
||||
element: 'line',
|
||||
attr: {
|
||||
id: id + '_vline_0',
|
||||
fill: 'none',
|
||||
stroke: sRgb,
|
||||
'stroke-width': sWidth,
|
||||
'stroke-linecap': 'round',
|
||||
x1: x0,
|
||||
y1: y0,
|
||||
x2: x0,
|
||||
y2: y0 + (fontSize + 6) * (items.length - 1)
|
||||
}
|
||||
});
|
||||
}
|
||||
newPM = svgCanvas.addSVGElementFromJson({
|
||||
element: 'g',
|
||||
attr: {
|
||||
id,
|
||||
class: 'placemark',
|
||||
fontSize,
|
||||
maxlen,
|
||||
lines: items.length,
|
||||
x: opts.start_x,
|
||||
y: opts.start_y,
|
||||
px: opts.start_x,
|
||||
py: opts.start_y
|
||||
},
|
||||
children
|
||||
});
|
||||
setMarker(
|
||||
newPM.firstElementChild,
|
||||
$('#placemark_marker').attr('value') || 'leftarrow'
|
||||
);
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
mouseMove (opts) {
|
||||
if (!started) {
|
||||
return undefined;
|
||||
}
|
||||
if (svgCanvas.getMode() === 'placemark') {
|
||||
const x = opts.mouse_x / svgCanvas.getZoom();
|
||||
const y = opts.mouse_y / svgCanvas.getZoom();
|
||||
const {fontSize, maxlen, lines, px, py} = $(newPM).attr(
|
||||
['fontSize', 'maxlen', 'lines', 'px', 'py']
|
||||
);
|
||||
$(newPM).attr({x, y});
|
||||
$(newPM).children().each((_, i) => {
|
||||
const [, , type, n] = i.id.split('_');
|
||||
const y0 = y + (fontSize + 6) * n,
|
||||
x0 = x + maxlen * fontSize * 0.5 + fontSize;
|
||||
const nx = (x + (x0 - x) / 2 < px) ? x0 : x;
|
||||
const ny = (y + ((fontSize + 6) * (lines - 1)) / 2 < py)
|
||||
? y + (fontSize + 6) * (lines - 1)
|
||||
: y;
|
||||
if (type === 'pline') {
|
||||
i.setAttribute('x2', nx);
|
||||
i.setAttribute('y2', ny);
|
||||
}
|
||||
if (type === 'tline') {
|
||||
i.setAttribute('x1', x);
|
||||
i.setAttribute('y1', y0);
|
||||
i.setAttribute('x2', x0);
|
||||
i.setAttribute('y2', y0);
|
||||
}
|
||||
if (type === 'vline') {
|
||||
i.setAttribute('x1', nx);
|
||||
i.setAttribute('y1', y);
|
||||
i.setAttribute('x2', nx);
|
||||
i.setAttribute('y2', y + (fontSize + 6) * (lines - 1));
|
||||
}
|
||||
if (type === 'txt') {
|
||||
i.setAttribute('x', x + fontSize / 2);
|
||||
i.setAttribute('y', y0 - 3);
|
||||
}
|
||||
});
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
mouseUp () {
|
||||
if (svgCanvas.getMode() === 'placemark') {
|
||||
const {x, y, px, py} = $(newPM).attr(['x', 'y', 'px', 'py']);
|
||||
return {
|
||||
keep: (x != px && y != py), // eslint-disable-line eqeqeq
|
||||
element: newPM
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
// Use this to update the current selected elements
|
||||
selElems = opts.elems;
|
||||
selElems.forEach((elem) => {
|
||||
if (elem && elem.getAttribute('class').includes('placemark')) {
|
||||
const txt = [];
|
||||
$(elem).children().each((n, i) => {
|
||||
const [, , type] = i.id.split('_');
|
||||
if (type === 'txt') {
|
||||
$('#placemarkFont').val(
|
||||
i.getAttribute('font-family') + ' ' + i.getAttribute('font-size')
|
||||
);
|
||||
txt.push($(i).text());
|
||||
}
|
||||
});
|
||||
$('#placemarkText').val(txt.join(';'));
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
elementChanged (opts) {
|
||||
opts.elems.forEach((elem) => {
|
||||
if (elem.id.includes('pline_0')) { // need update marker of pline_0
|
||||
colorChanged(elem);
|
||||
updateReferences(elem);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
303
src/editor/extensions/ext-polygon.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* @file ext-polygon.js
|
||||
*
|
||||
*
|
||||
* @copyright 2010 CloudCanvas, Inc. All rights reserved
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'polygon',
|
||||
async init (S) {
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const {$, importLocale} = S, // {svgcontent}
|
||||
// addElem = svgCanvas.addSVGElementFromJson,
|
||||
editingitex = false;
|
||||
const strings = await importLocale();
|
||||
let selElems,
|
||||
// svgdoc = S.svgroot.parentNode.ownerDocument,
|
||||
// newFOG, newFOGParent, newDef, newImageName, newMaskID, modeChangeG,
|
||||
// edg = 0,
|
||||
// undoCommand = 'Not image';
|
||||
started, newFO;
|
||||
|
||||
// const ccZoom;
|
||||
// const wEl, hEl;
|
||||
// const wOffset, hOffset;
|
||||
// const ccRBG;
|
||||
// const ccOpacity;
|
||||
// const brushW, brushH;
|
||||
|
||||
// const ccDebug = document.getElementById('debugpanel');
|
||||
|
||||
/* const properlySourceSizeTextArea = function(){
|
||||
// TODO: remove magic numbers here and get values from CSS
|
||||
const height = $('#svg_source_container').height() - 80;
|
||||
$('#svg_source_textarea').css('height', height);
|
||||
}; */
|
||||
|
||||
/**
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function showPanel (on) {
|
||||
let fcRules = $('#fc_rules');
|
||||
if (!fcRules.length) {
|
||||
fcRules = $('<style id="fc_rules"></style>').appendTo('head');
|
||||
}
|
||||
fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }');
|
||||
$('#polygon_panel').toggle(on);
|
||||
}
|
||||
|
||||
/*
|
||||
function toggleSourceButtons(on){
|
||||
$('#tool_source_save, #tool_source_cancel').toggle(!on);
|
||||
$('#polygon_save, #polygon_cancel').toggle(on);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} attr
|
||||
* @param {string|Float} val
|
||||
* @returns {void}
|
||||
*/
|
||||
function setAttr (attr, val) {
|
||||
svgCanvas.changeSelectedAttribute(attr, val);
|
||||
svgCanvas.call('changed', selElems);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Float} n
|
||||
* @returns {Float}
|
||||
*/
|
||||
function cot (n) {
|
||||
return 1 / Math.tan(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Float} n
|
||||
* @returns {Float}
|
||||
*/
|
||||
function sec (n) {
|
||||
return 1 / Math.cos(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtained from http://code.google.com/p/passenger-top/source/browse/instiki/public/svg-edit/editor/extensions/ext-itex.js?r=3
|
||||
* This function sets the content of of the currently-selected foreignObject element,
|
||||
* based on the itex contained in string.
|
||||
* @param {string} tex The itex text.
|
||||
* @returns {boolean} This function returns false if the set was unsuccessful, true otherwise.
|
||||
*/
|
||||
/*
|
||||
function setItexString(tex) {
|
||||
const mathns = 'http://www.w3.org/1998/Math/MathML',
|
||||
xmlnsns = 'http://www.w3.org/2000/xmlns/',
|
||||
ajaxEndpoint = '../../itex';
|
||||
const elt = selElems[0];
|
||||
try {
|
||||
const math = svgdoc.createElementNS(mathns, 'math');
|
||||
math.setAttributeNS(xmlnsns, 'xmlns', mathns);
|
||||
math.setAttribute('display', 'inline');
|
||||
const semantics = document.createElementNS(mathns, 'semantics');
|
||||
const annotation = document.createElementNS(mathns, 'annotation');
|
||||
annotation.setAttribute('encoding', 'application/x-tex');
|
||||
annotation.textContent = tex;
|
||||
const mrow = document.createElementNS(mathns, 'mrow');
|
||||
semantics.append(mrow, annotation);
|
||||
math.append(semantics);
|
||||
// make an AJAX request to the server, to get the MathML
|
||||
$.post(ajaxEndpoint, {tex, display: 'inline'}, function(data){
|
||||
const children = data.documentElement.childNodes;
|
||||
while (children.length > 0) {
|
||||
mrow.append(svgdoc.adoptNode(children[0], true));
|
||||
}
|
||||
svgCanvas.sanitizeSvg(math);
|
||||
svgCanvas.call('changed', [elt]);
|
||||
});
|
||||
elt.firstChild.replaceWith(math);
|
||||
svgCanvas.call('changed', [elt]);
|
||||
svgCanvas.clearSelection();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
const buttons = [{
|
||||
id: 'tool_polygon',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'polygon.png',
|
||||
type: 'mode',
|
||||
position: 11,
|
||||
events: {
|
||||
click () {
|
||||
svgCanvas.setMode('polygon');
|
||||
showPanel(true);
|
||||
}
|
||||
}
|
||||
}];
|
||||
const contextTools = [{
|
||||
type: 'input',
|
||||
panel: 'polygon_panel',
|
||||
id: 'polySides',
|
||||
size: 3,
|
||||
defval: 5,
|
||||
events: {
|
||||
change () {
|
||||
setAttr('sides', this.value);
|
||||
}
|
||||
}
|
||||
}];
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'polygon-icons.svg',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
context_tools: strings.contextTools.map((contextTool, i) => {
|
||||
return Object.assign(contextTools[i], contextTool);
|
||||
}),
|
||||
|
||||
callback () {
|
||||
$('#polygon_panel').hide();
|
||||
|
||||
const endChanges = function () {
|
||||
// Todo: Missing?
|
||||
};
|
||||
|
||||
// TODO: Needs to be done after orig icon loads
|
||||
setTimeout(function () {
|
||||
// Create source save/cancel buttons
|
||||
/* const save = */ $('#tool_source_save').clone().hide().attr('id', 'polygon_save').unbind().appendTo('#tool_source_back').click(function () {
|
||||
if (!editingitex) {
|
||||
return;
|
||||
}
|
||||
// Todo: Uncomment the setItexString() function above and handle ajaxEndpoint?
|
||||
/*
|
||||
if (!setItexString($('#svg_source_textarea').val())) {
|
||||
const ok = await $.confirm('Errors found. Revert to original?', function (ok) {
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
endChanges();
|
||||
} else { */
|
||||
endChanges();
|
||||
// }
|
||||
// setSelectMode();
|
||||
});
|
||||
|
||||
/* const cancel = */ $('#tool_source_cancel').clone().hide().attr('id', 'polygon_cancel').unbind().appendTo('#tool_source_back').click(function () {
|
||||
endChanges();
|
||||
});
|
||||
}, 3000);
|
||||
},
|
||||
mouseDown (opts) {
|
||||
if (svgCanvas.getMode() !== 'polygon') {
|
||||
return undefined;
|
||||
}
|
||||
// const e = opts.event;
|
||||
const rgb = svgCanvas.getColor('fill');
|
||||
// const ccRgbEl = rgb.substring(1, rgb.length);
|
||||
const sRgb = svgCanvas.getColor('stroke');
|
||||
// ccSRgbEl = sRgb.substring(1, rgb.length);
|
||||
const sWidth = svgCanvas.getStrokeWidth();
|
||||
|
||||
started = true;
|
||||
|
||||
newFO = svgCanvas.addSVGElementFromJson({
|
||||
element: 'polygon',
|
||||
attr: {
|
||||
cx: opts.start_x,
|
||||
cy: opts.start_y,
|
||||
id: svgCanvas.getNextId(),
|
||||
shape: 'regularPoly',
|
||||
sides: document.getElementById('polySides').value,
|
||||
orient: 'x',
|
||||
edge: 0,
|
||||
fill: rgb,
|
||||
strokecolor: sRgb,
|
||||
strokeWidth: sWidth
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
},
|
||||
mouseMove (opts) {
|
||||
if (!started || svgCanvas.getMode() !== 'polygon') {
|
||||
return undefined;
|
||||
}
|
||||
// const e = opts.event;
|
||||
const c = $(newFO).attr(['cx', 'cy', 'sides', 'orient', 'fill', 'strokecolor', 'strokeWidth']);
|
||||
let x = opts.mouse_x;
|
||||
let y = opts.mouse_y;
|
||||
const {cx, cy, fill, strokecolor, strokeWidth, sides} = c, // {orient} = c,
|
||||
edg = (Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))) / 1.5;
|
||||
newFO.setAttribute('edge', edg);
|
||||
|
||||
const inradius = (edg / 2) * cot(Math.PI / sides);
|
||||
const circumradius = inradius * sec(Math.PI / sides);
|
||||
let points = '';
|
||||
for (let s = 0; sides >= s; s++) {
|
||||
const angle = 2.0 * Math.PI * s / sides;
|
||||
x = (circumradius * Math.cos(angle)) + cx;
|
||||
y = (circumradius * Math.sin(angle)) + cy;
|
||||
|
||||
points += x + ',' + y + ' ';
|
||||
}
|
||||
|
||||
// const poly = newFO.createElementNS(NS.SVG, 'polygon');
|
||||
newFO.setAttribute('points', points);
|
||||
newFO.setAttribute('fill', fill);
|
||||
newFO.setAttribute('stroke', strokecolor);
|
||||
newFO.setAttribute('stroke-width', strokeWidth);
|
||||
// newFO.setAttribute('transform', 'rotate(-90)');
|
||||
// const shape = newFO.getAttribute('shape');
|
||||
// newFO.append(poly);
|
||||
// DrawPoly(cx, cy, sides, edg, orient);
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
},
|
||||
|
||||
mouseUp (opts) {
|
||||
if (svgCanvas.getMode() !== 'polygon') {
|
||||
return undefined;
|
||||
}
|
||||
const attrs = $(newFO).attr('edge');
|
||||
const keep = (attrs.edge !== '0');
|
||||
// svgCanvas.addToSelection([newFO], true);
|
||||
return {
|
||||
keep,
|
||||
element: newFO
|
||||
};
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
// Use this to update the current selected elements
|
||||
selElems = opts.elems;
|
||||
|
||||
let i = selElems.length;
|
||||
while (i--) {
|
||||
const elem = selElems[i];
|
||||
if (elem && elem.getAttribute('shape') === 'regularPoly') {
|
||||
if (opts.selectedElement && !opts.multiselected) {
|
||||
$('#polySides').val(elem.getAttribute('sides'));
|
||||
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
elementChanged (opts) {
|
||||
// const elem = opts.elems[0];
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
60
src/editor/extensions/ext-server_moinsave.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file ext-server_moinsave.js
|
||||
*
|
||||
* @license (MIT OR GPL-2.0-or-later)
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria, 2011 MoinMoin:ReimarBauer
|
||||
* adopted for moinmoins item storage. It sends in one post png and svg data
|
||||
* (I agree to dual license my work to additional GPLv2 or later)
|
||||
*/
|
||||
import {canvg} from '../canvg/canvg.js';
|
||||
|
||||
export default {
|
||||
name: 'server_moinsave',
|
||||
async init ({$, encode64, importLocale}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
const saveSvgAction = '/+modify';
|
||||
|
||||
// Create upload target (hidden iframe)
|
||||
// Hiding by size instead of display to avoid FF console errors
|
||||
// with `getBBox` in browser.js `supportsPathBBox_`)
|
||||
/* const target = */ $(
|
||||
`<iframe name="output_frame" title="${strings.hiddenframe}"
|
||||
style="width: 0; height: 0;" src="data:text/html;base64,PGh0bWw+PC9odG1sPg=="/>`
|
||||
).appendTo('body');
|
||||
|
||||
svgEditor.setCustomHandlers({
|
||||
async save (win, data) {
|
||||
const svg = '<?xml version="1.0"?>\n' + data;
|
||||
const {pathname} = new URL(location);
|
||||
const name = pathname.replace(/\/+get\//, '');
|
||||
const svgData = encode64(svg);
|
||||
if (!$('#export_canvas').length) {
|
||||
$('<canvas>', {id: 'export_canvas'}).hide().appendTo('body');
|
||||
}
|
||||
const c = $('#export_canvas')[0];
|
||||
c.width = svgCanvas.contentW;
|
||||
c.height = svgCanvas.contentH;
|
||||
await canvg(c, svg);
|
||||
const datauri = c.toDataURL('image/png');
|
||||
// const {uiStrings} = svgEditor;
|
||||
const pngData = encode64(datauri); // Brett: This encoding seems unnecessary
|
||||
/* const form = */ $('<form>').attr({
|
||||
method: 'post',
|
||||
action: saveSvgAction + '/' + name,
|
||||
target: 'output_frame'
|
||||
}).append(`
|
||||
<input type="hidden" name="png_data" value="${pngData}">
|
||||
<input type="hidden" name="filepath" value="${svgData}">
|
||||
<input type="hidden" name="filename" value="drawing.svg">
|
||||
<input type="hidden" name="contenttype" value="application/x-svgdraw">
|
||||
`).appendTo('body')
|
||||
.submit().remove();
|
||||
$.alert(strings.saved);
|
||||
top.window.location = '/' + name;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
269
src/editor/extensions/ext-server_opensave.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* @file ext-server_opensave.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
import {canvg} from '../canvg/canvg.js';
|
||||
|
||||
export default {
|
||||
name: 'server_opensave',
|
||||
async init ({$, decode64, encode64, importLocale}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const {
|
||||
curConfig: {
|
||||
extPath,
|
||||
avoidClientSide, // Deprecated
|
||||
avoidClientSideDownload, avoidClientSideOpen
|
||||
},
|
||||
canvas: svgCanvas
|
||||
} = svgEditor;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function getFileNameFromTitle () {
|
||||
const title = svgCanvas.getDocumentTitle();
|
||||
// We convert (to underscore) only those disallowed Win7 file name characters
|
||||
return title.trim().replace(/[/\\:*?"<>|]/g, '_');
|
||||
}
|
||||
/**
|
||||
* Escapes XML predefined entities for quoted attributes.
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function xhtmlEscape (str) {
|
||||
return str.replace(/&(?!amp;)/g, '&').replace(/"/g, '"').replace(/</g, '<'); // < is actually disallowed above anyways
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [filename='image']
|
||||
* @param {string} suffix To add to file name
|
||||
* @param {string} uri
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function clientDownloadSupport (filename, suffix, uri) {
|
||||
if (avoidClientSide || avoidClientSideDownload) {
|
||||
return false;
|
||||
}
|
||||
const support = $('<a>')[0].download === '';
|
||||
let a;
|
||||
if (support) {
|
||||
a = $('<a>hidden</a>').attr({
|
||||
download: (filename || 'image') + suffix,
|
||||
href: uri
|
||||
}).css('display', 'none').appendTo('body');
|
||||
a[0].click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const
|
||||
saveSvgAction = extPath + 'filesave.php',
|
||||
saveImgAction = extPath + 'filesave.php';
|
||||
// Create upload target (hidden iframe)
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
// Hiding by size instead of display to avoid FF console errors
|
||||
// with `getBBox` in browser.js `supportsPathBBox_`)
|
||||
$(
|
||||
`<iframe name="output_frame" title="${strings.hiddenframe}"
|
||||
style="width: 0; height: 0;" src="data:text/html;base64,PGh0bWw+"/>`
|
||||
).appendTo('body');
|
||||
svgEditor.setCustomHandlers({
|
||||
save (win, data) {
|
||||
const svg = '<?xml version="1.0" encoding="UTF-8"?>\n' + data, // Firefox doesn't seem to know it is UTF-8 (no matter whether we use or skip the clientDownload code) despite the Content-Disposition header containing UTF-8, but adding the encoding works
|
||||
filename = getFileNameFromTitle();
|
||||
|
||||
if (clientDownloadSupport(filename, '.svg', 'data:image/svg+xml;charset=UTF-8;base64,' + encode64(svg))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('<form>').attr({
|
||||
method: 'post',
|
||||
action: saveSvgAction,
|
||||
target: 'output_frame'
|
||||
}).append(`
|
||||
<input type="hidden" name="output_svg" value="${xhtmlEscape(svg)}">
|
||||
<input type="hidden" name="filename" value="${xhtmlEscape(filename)}">
|
||||
`).appendTo('body')
|
||||
.submit().remove();
|
||||
},
|
||||
exportPDF (win, data) {
|
||||
const filename = getFileNameFromTitle(),
|
||||
datauri = data.output;
|
||||
if (clientDownloadSupport(filename, '.pdf', datauri)) {
|
||||
return;
|
||||
}
|
||||
$('<form>').attr({
|
||||
method: 'post',
|
||||
action: saveImgAction,
|
||||
target: 'output_frame'
|
||||
}).append(`
|
||||
<input type="hidden" name="output_img" value="${datauri}">
|
||||
<input type="hidden" name="mime" value="application/pdf">
|
||||
<input type="hidden" name="filename" value="${xhtmlEscape(filename)}">
|
||||
`).appendTo('body')
|
||||
.submit().remove();
|
||||
},
|
||||
// Todo: Integrate this extension with a new built-in exportWindowType, "download"
|
||||
async exportImage (win, data) {
|
||||
const {issues, mimeType, quality} = data;
|
||||
|
||||
if (!$('#export_canvas').length) {
|
||||
$('<canvas>', {id: 'export_canvas'}).hide().appendTo('body');
|
||||
}
|
||||
const c = $('#export_canvas')[0];
|
||||
|
||||
c.width = svgCanvas.contentW;
|
||||
c.height = svgCanvas.contentH;
|
||||
await canvg(c, data.svg);
|
||||
const datauri = quality ? c.toDataURL(mimeType, quality) : c.toDataURL(mimeType);
|
||||
// {uiStrings} = svgEditor;
|
||||
|
||||
// Check if there are issues
|
||||
let pre, note = '';
|
||||
if (issues.length) {
|
||||
pre = '\n \u2022 '; // Bullet
|
||||
note += ('\n\n' + pre + issues.join(pre));
|
||||
}
|
||||
|
||||
if (note.length) {
|
||||
await $.alert(note);
|
||||
}
|
||||
|
||||
const filename = getFileNameFromTitle();
|
||||
const suffix = '.' + data.type.toLowerCase();
|
||||
|
||||
if (clientDownloadSupport(filename, suffix, datauri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('<form>').attr({
|
||||
method: 'post',
|
||||
action: saveImgAction,
|
||||
target: 'output_frame'
|
||||
}).append(`
|
||||
<input type="hidden" name="output_img" value="${datauri}">
|
||||
<input type="hidden" name="mime" value="${mimeType}">
|
||||
<input type="hidden" name="filename" value="${xhtmlEscape(filename)}">
|
||||
`).appendTo('body')
|
||||
.submit().remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Do nothing if client support is found
|
||||
if (window.FileReader && !avoidClientSideOpen) { return; }
|
||||
|
||||
// Change these to appropriate script file
|
||||
const openSvgAction = extPath + 'fileopen.php?type=load_svg';
|
||||
const importSvgAction = extPath + 'fileopen.php?type=import_svg';
|
||||
const importImgAction = extPath + 'fileopen.php?type=import_img';
|
||||
|
||||
// Set up function for PHP uploader to use
|
||||
svgEditor.processFile = function (str64, type) {
|
||||
let xmlstr;
|
||||
if (cancelled) {
|
||||
cancelled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$('#dialog_box').hide();
|
||||
|
||||
if (type !== 'import_img') {
|
||||
xmlstr = decode64(str64);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'load_svg':
|
||||
svgCanvas.clear();
|
||||
svgCanvas.setSvgString(xmlstr);
|
||||
svgEditor.updateCanvas();
|
||||
break;
|
||||
case 'import_svg':
|
||||
svgCanvas.importSvgString(xmlstr);
|
||||
svgEditor.updateCanvas();
|
||||
break;
|
||||
case 'import_img':
|
||||
svgCanvas.setGoodImage(str64);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Create upload form
|
||||
const openSvgForm = $('<form>');
|
||||
openSvgForm.attr({
|
||||
enctype: 'multipart/form-data',
|
||||
method: 'post',
|
||||
action: openSvgAction,
|
||||
target: 'output_frame'
|
||||
});
|
||||
|
||||
// Create import form
|
||||
const importSvgForm = openSvgForm.clone().attr('action', importSvgAction);
|
||||
|
||||
// Create image form
|
||||
const importImgForm = openSvgForm.clone().attr('action', importImgAction);
|
||||
|
||||
// It appears necessary to rebuild this input every time a file is
|
||||
// selected so the same file can be picked and the change event can fire.
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {external:jQuery} form
|
||||
* @returns {void}
|
||||
*/
|
||||
function rebuildInput (form) {
|
||||
form.empty();
|
||||
const inp = $('<input type="file" name="svg_file">').appendTo(form);
|
||||
|
||||
/**
|
||||
* Submit the form, empty its contents for reuse and show
|
||||
* uploading message.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function submit () {
|
||||
// This submits the form, which returns the file data using `svgEditor.processFile()`
|
||||
form.submit();
|
||||
|
||||
rebuildInput(form);
|
||||
await $.process_cancel(strings.uploading);
|
||||
cancelled = true;
|
||||
$('#dialog_box').hide();
|
||||
}
|
||||
|
||||
if (form[0] === openSvgForm[0]) {
|
||||
inp.change(async function () {
|
||||
// This takes care of the "are you sure" dialog box
|
||||
const ok = await svgEditor.openPrep();
|
||||
if (!ok) {
|
||||
rebuildInput(form);
|
||||
return;
|
||||
}
|
||||
await submit();
|
||||
});
|
||||
} else {
|
||||
inp.change(async function () {
|
||||
// This submits the form, which returns the file data using svgEditor.processFile()
|
||||
await submit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create the input elements
|
||||
rebuildInput(openSvgForm);
|
||||
rebuildInput(importSvgForm);
|
||||
rebuildInput(importImgForm);
|
||||
|
||||
// Add forms to buttons
|
||||
$('#tool_open').show().prepend(openSvgForm);
|
||||
$('#tool_import').show().prepend(importSvgForm);
|
||||
$('#tool_image').prepend(importImgForm);
|
||||
}
|
||||
};
|
||||
376
src/editor/extensions/ext-shapes.js
Normal file
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* @file ext-shapes.js
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Christian Tzurcanu, 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'shapes',
|
||||
async init ({$, importLocale}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const canv = svgEditor.canvas;
|
||||
const svgroot = canv.getRootElem();
|
||||
let lastBBox = {};
|
||||
|
||||
// This populates the category list
|
||||
const {categories} = strings;
|
||||
|
||||
const library = {
|
||||
basic: {
|
||||
data: {
|
||||
heart: 'm150,73c61,-175 300,0 0,225c-300,-225 -61,-400 0,-225z',
|
||||
frame: 'm0,0l300,0l0,300l-300,0zm35,-265l0,230l230,0l0,-230z',
|
||||
donut: 'm1,150l0,0c0,-82.29042 66.70958,-149 149,-149l0,0c39.51724,0 77.41599,15.69816 105.35889,43.64108c27.94293,27.94293 43.64111,65.84165 43.64111,105.35892l0,0c0,82.29041 -66.70958,149 -149,149l0,0c-82.29041,0 -149,-66.70959 -149,-149zm74.5,0l0,0c0,41.1452 33.35481,74.5 74.5,74.5c41.14522,0 74.5,-33.3548 74.5,-74.5c0,-41.1452 -33.3548,-74.5 -74.5,-74.5l0,0c-41.14519,0 -74.5,33.35481 -74.5,74.5z',
|
||||
triangle: 'm1,280.375l149,-260.75l149,260.75z',
|
||||
right_triangle: 'm1,299l0,-298l298,298z',
|
||||
diamond: 'm1,150l149,-149l149,149l-149,149l-149,-149z',
|
||||
pentagon: 'm1.00035,116.97758l148.99963,-108.4053l148.99998,108.4053l-56.91267,175.4042l-184.1741,0l-56.91284,-175.4042z',
|
||||
hexagon: 'm1,149.99944l63.85715,-127.71428l170.28572,0l63.85713,127.71428l-63.85713,127.71428l-170.28572,0l-63.85715,-127.71428z',
|
||||
septagon1: 'm0.99917,191.06511l29.51249,-127.7108l119.48833,-56.83673l119.48836,56.83673l29.51303,127.7108l-82.69087,102.41679l-132.62103,0l-82.69031,-102.41679z',
|
||||
heptagon: 'm1,88.28171l87.28172,-87.28171l123.43653,0l87.28172,87.28171l0,123.43654l-87.28172,87.28172l-123.43653,0l-87.28172,-87.28172l0,-123.43654z',
|
||||
decagon: 'm1,150.00093l28.45646,-88.40318l74.49956,-54.63682l92.08794,0l74.50002,54.63682l28.45599,88.40318l-28.45599,88.40318l-74.50002,54.63681l-92.08794,0l-74.49956,-54.63681l-28.45646,-88.40318z',
|
||||
dodecagon: 'm1,110.07421l39.92579,-69.14842l69.14842,-39.92579l79.85159,0l69.14842,39.92579l39.92578,69.14842l0,79.85159l-39.92578,69.14842l-69.14842,39.92578l-79.85159,0l-69.14842,-39.92578l-39.92579,-69.14842l0,-79.85159z',
|
||||
star_points_5: 'm1,116.58409l113.82668,0l35.17332,-108.13487l35.17334,108.13487l113.82666,0l-92.08755,66.83026l35.17514,108.13487l-92.08759,-66.83208l-92.08757,66.83208l35.17515,-108.13487l-92.08758,-66.83026z',
|
||||
trapezoid: 'm1,299l55.875,-298l186.25001,0l55.87498,298z',
|
||||
arrow_up: 'm1.49805,149.64304l148.50121,-148.00241l148.50121,148.00241l-74.25061,0l0,148.71457l-148.5012,0l0,-148.71457z',
|
||||
vertical_scrool: 'm37.375,261.625l0,-242.9375l0,0c0,-10.32083 8.36669,-18.6875 18.6875,-18.6875l224.25,0c10.32083,0 18.6875,8.36667 18.6875,18.6875c0,10.32081 -8.36667,18.6875 -18.6875,18.6875l-18.6875,0l0,242.9375c0,10.32083 -8.36668,18.6875 -18.6875,18.6875l-224.25,0l0,0c-10.32083,0 -18.6875,-8.36667 -18.6875,-18.6875c0,-10.32083 8.36667,-18.6875 18.6875,-18.6875zm37.375,-261.625l0,0c10.32081,0 18.6875,8.36667 18.6875,18.6875c0,10.32081 -8.36669,18.6875 -18.6875,18.6875c-5.1604,0 -9.34375,-4.18335 -9.34375,-9.34375c0,-5.16041 4.18335,-9.34375 9.34375,-9.34375l18.6875,0m186.875,18.6875l-205.5625,0m-37.375,224.25l0,0c5.1604,0 9.34375,4.18335 9.34375,9.34375c0,5.1604 -4.18335,9.34375 -9.34375,9.34375l18.6875,0m-18.6875,18.6875l0,0c10.32081,0 18.6875,-8.36667 18.6875,-18.6875l0,-18.6875',
|
||||
smiley: 'm68.49886,214.78838q81.06408,55.67332 161.93891,0m-144.36983,-109.9558c0,-8.60432 6.97517,-15.57949 15.57948,-15.57949c8.60431,0 15.57948,6.97517 15.57948,15.57949c0,8.60431 -6.97517,15.57947 -15.57948,15.57947c-8.60431,0 -15.57948,-6.97516 -15.57948,-15.57947m95.83109,0c0,-8.60432 6.97517,-15.57949 15.57948,-15.57949c8.60431,0 15.57947,6.97517 15.57947,15.57949c0,8.60431 -6.97516,15.57947 -15.57947,15.57947c-8.60429,0 -15.57948,-6.97516 -15.57948,-15.57947m-181.89903,44.73038l0,0c0,-82.60133 66.96162,-149.56296 149.56296,-149.56296c82.60135,0 149.56296,66.96162 149.56296,149.56296c0,82.60135 -66.96161,149.56296 -149.56296,149.56296c-82.60133,0 -149.56296,-66.96161 -149.56296,-149.56296zm0,0l0,0c0,-82.60133 66.96162,-149.56296 149.56296,-149.56296c82.60135,0 149.56296,66.96162 149.56296,149.56296c0,82.60135 -66.96161,149.56296 -149.56296,149.56296c-82.60133,0 -149.56296,-66.96161 -149.56296,-149.56296z',
|
||||
left_braket: 'm174.24565,298.5c-13.39009,0 -24.24489,-1.80908 -24.24489,-4.04065l0,-140.4187c0,-2.23158 -10.85481,-4.04065 -24.2449,-4.04065l0,0c13.39009,0 24.2449,-1.80907 24.2449,-4.04065l0,-140.4187l0,0c0,-2.23159 10.8548,-4.04066 24.24489,-4.04066',
|
||||
uml_actor: 'm40.5,100l219,0m-108.99991,94.00006l107,105m-107.00009,-106.00006l-100,106m99.5,-231l0,125m33.24219,-158.75781c0,18.35916 -14.88303,33.24219 -33.24219,33.24219c-18.35916,0 -33.2422,-14.88303 -33.2422,-33.24219c0.00002,-18.35915 14.88304,-33.24219 33.2422,-33.24219c18.35916,0 33.24219,14.88304 33.24219,33.24219z',
|
||||
dialog_balloon_1: 'm0.99786,35.96579l0,0c0,-19.31077 15.28761,-34.96524 34.14583,-34.96524l15.52084,0l0,0l74.50001,0l139.68748,0c9.05606,0 17.74118,3.68382 24.14478,10.24108c6.40356,6.55726 10.00107,15.45081 10.00107,24.72416l0,87.41311l0,0l0,52.44785l0,0c0,19.31078 -15.2876,34.96524 -34.14584,34.96524l-139.68748,0l-97.32507,88.90848l22.82506,-88.90848l-15.52084,0c-18.85822,0 -34.14583,-15.65446 -34.14583,-34.96524l0,0l0,-52.44785l0,0z',
|
||||
cloud: 'm182.05086,34.31005c-0.64743,0.02048 -1.27309,0.07504 -1.92319,0.13979c-10.40161,1.03605 -19.58215,7.63722 -24.24597,17.4734l-2.47269,7.44367c0.53346,-2.57959 1.35258,-5.08134 2.47269,-7.44367c-8.31731,-8.61741 -19.99149,-12.59487 -31.52664,-10.72866c-11.53516,1.8662 -21.55294,9.3505 -27.02773,20.19925c-15.45544,-9.51897 -34.72095,-8.94245 -49.62526,1.50272c-14.90431,10.44516 -22.84828,28.93916 -20.43393,47.59753l1.57977,7.58346c-0.71388,-2.48442 -1.24701,-5.01186 -1.57977,-7.58346l-0.2404,0.69894c-12.95573,1.4119 -23.58103,11.46413 -26.34088,24.91708c-2.75985,13.45294 2.9789,27.25658 14.21789,34.21291l17.54914,4.26352c-6.1277,0.50439 -12.24542,-0.9808 -17.54914,-4.26352c-8.66903,9.71078 -10.6639,24.08736 -4.94535,35.96027c5.71854,11.87289 17.93128,18.70935 30.53069,17.15887l7.65843,-2.02692c-2.46413,1.0314 -5.02329,1.70264 -7.65843,2.02692c7.15259,13.16728 19.01251,22.77237 32.93468,26.5945c13.92217,3.82214 28.70987,1.56322 41.03957,-6.25546c10.05858,15.86252 27.91113,24.19412 45.81322,21.38742c17.90208,-2.8067 32.66954,-16.26563 37.91438,-34.52742l1.82016,-10.20447c-0.27254,3.46677 -0.86394,6.87508 -1.82016,10.20447c12.31329,8.07489 27.80199,8.52994 40.52443,1.18819c12.72244,-7.34175 20.6609,-21.34155 20.77736,-36.58929l-4.56108,-22.7823l-17.96776,-15.41455c13.89359,8.70317 22.6528,21.96329 22.52884,38.19685c16.5202,0.17313 30.55292,-13.98268 36.84976,-30.22897c6.29684,-16.24631 3.91486,-34.76801 -6.2504,-48.68089c4.21637,-10.35873 3.96622,-22.14172 -0.68683,-32.29084c-4.65308,-10.14912 -13.23602,-17.69244 -23.55914,-20.65356c-2.31018,-13.45141 -11.83276,-24.27162 -24.41768,-27.81765c-12.58492,-3.54603 -25.98557,0.82654 -34.41142,11.25287l-5.11707,8.63186c1.30753,-3.12148 3.01521,-6.03101 5.11707,-8.63186c-5.93959,-8.19432 -15.2556,-12.8181 -24.96718,-12.51096z',
|
||||
cylinder: 'm299.0007,83.77844c0,18.28676 -66.70958,33.11111 -149.00002,33.11111m149.00002,-33.11111l0,0c0,18.28676 -66.70958,33.11111 -149.00002,33.11111c-82.29041,0 -148.99997,-14.82432 -148.99997,-33.11111m0,0l0,0c0,-18.28674 66.70956,-33.1111 148.99997,-33.1111c82.29044,0 149.00002,14.82436 149.00002,33.1111l0,132.44449c0,18.28674 -66.70958,33.11105 -149.00002,33.11105c-82.29041,0 -148.99997,-14.82431 -148.99997,-33.11105z',
|
||||
arrow_u_turn: 'm1.00059,299.00055l0,-167.62497l0,0c0,-72.00411 58.37087,-130.37499 130.375,-130.37499l0,0l0,0c34.57759,0 67.73898,13.7359 92.18906,38.18595c24.45006,24.45005 38.18593,57.61144 38.18593,92.18904l0,18.625l37.24997,0l-74.49995,74.50002l-74.50002,-74.50002l37.25,0l0,-18.625c0,-30.8589 -25.0161,-55.87498 -55.87498,-55.87498l0,0l0,0c-30.85892,0 -55.875,25.01608 -55.875,55.87498l0,167.62497z',
|
||||
arrow_left_up: 'm0.99865,224.5l74.50004,-74.5l0,37.25l111.74991,0l0,-111.75l-37.25,0l74.5,-74.5l74.5,74.5l-37.25,0l0,186.25l-186.24989,0l0,37.25l-74.50005,-74.5z',
|
||||
maximize: 'm1.00037,150.34581l55.30305,-55.30267l0,27.65093l22.17356,0l0,-44.21833l44.21825,0l0,-22.17357l-27.65095,0l55.30267,-55.30292l55.3035,55.30292l-27.65175,0l0,22.17357l44.21835,0l0,44.21833l22.17357,0l0,-27.65093l55.30345,55.30267l-55.30345,55.3035l0,-27.65175l-22.17357,0l0,44.21834l-44.21835,0l0,22.17355l27.65175,0l-55.3035,55.30348l-55.30267,-55.30348l27.65095,0l0,-22.17355l-44.21825,0l0,-44.21834l-22.17356,0l0,27.65175l-55.30305,-55.3035z',
|
||||
cross: 'm0.99844,99.71339l98.71494,0l0,-98.71495l101.26279,0l0,98.71495l98.71495,0l0,101.2628l-98.71495,0l0,98.71494l-101.26279,0l0,-98.71494l-98.71494,0z',
|
||||
plaque: 'm-0.00197,49.94376l0,0c27.5829,0 49.94327,-22.36036 49.94327,-49.94327l199.76709,0l0,0c0,27.5829 22.36037,49.94327 49.94325,49.94327l0,199.7671l0,0c-27.58289,0 -49.94325,22.36034 -49.94325,49.94325l-199.76709,0c0,-27.58292 -22.36037,-49.94325 -49.94327,-49.94325z',
|
||||
page: 'm249.3298,298.99744l9.9335,-39.73413l39.73413,-9.93355l-49.66763,49.66768l-248.33237,0l0,-298.00001l298.00001,0l0,248.33234'
|
||||
|
||||
},
|
||||
buttons: []
|
||||
}
|
||||
};
|
||||
|
||||
const modeId = 'shapelib';
|
||||
const startClientPos = {};
|
||||
|
||||
let currentD, curShapeId, curShape, startX, startY;
|
||||
let curLib = library.basic;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function loadIcons () {
|
||||
$('#shape_buttons').empty().append(curLib.buttons);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {PlainObject} module:Extension.Shapes.Shapes
|
||||
* @property {PlainObject<string, string>} data
|
||||
* @property {Integer} [size]
|
||||
* @property {boolean} [fill]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string|"basic"} cat Category ID
|
||||
* @param {module:Extension.Shapes.Shapes} shapes
|
||||
* @returns {void}
|
||||
*/
|
||||
function makeButtons (cat, shapes) {
|
||||
const size = curLib.size || 300;
|
||||
const fill = curLib.fill || false;
|
||||
const off = size * 0.05;
|
||||
const vb = [-off, -off, size + off * 2, size + off * 2].join(' ');
|
||||
const stroke = fill ? 0 : (size / 30);
|
||||
const shapeIcon = new DOMParser().parseFromString(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<svg viewBox="' + vb + '">' +
|
||||
'<path fill="' + (fill ? '#333' : 'none') +
|
||||
'" stroke="#000" stroke-width="' + stroke + '" /></svg></svg>',
|
||||
'text/xml'
|
||||
);
|
||||
|
||||
const width = 24;
|
||||
const height = 24;
|
||||
shapeIcon.documentElement.setAttribute('width', width);
|
||||
shapeIcon.documentElement.setAttribute('height', height);
|
||||
const svgElem = $(document.importNode(shapeIcon.documentElement, true));
|
||||
|
||||
const {data} = shapes;
|
||||
|
||||
curLib.buttons = Object.entries(data).map(([id, pathD]) => {
|
||||
const icon = svgElem.clone();
|
||||
icon.find('path').attr('d', pathD);
|
||||
|
||||
const iconBtn = icon.wrap('<div class="tool_button">').parent().attr({
|
||||
id: modeId + '_' + id,
|
||||
title: id
|
||||
});
|
||||
// Store for later use
|
||||
return iconBtn[0];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|"basic"} catId
|
||||
* @returns {void}
|
||||
*/
|
||||
function loadLibrary (catId) {
|
||||
const lib = library[catId];
|
||||
|
||||
if (!lib) {
|
||||
$('#shape_buttons').html(strings.loading);
|
||||
$.getJSON(svgEditor.curConfig.extIconsPath + 'shapelib/' + catId + '.json', function (result) {
|
||||
curLib = library[catId] = {
|
||||
data: result.data,
|
||||
size: result.size,
|
||||
fill: result.fill
|
||||
};
|
||||
makeButtons(catId, result);
|
||||
loadIcons();
|
||||
});
|
||||
return;
|
||||
}
|
||||
curLib = lib;
|
||||
if (!lib.buttons.length) { makeButtons(catId, lib); }
|
||||
loadIcons();
|
||||
}
|
||||
const buttons = [{
|
||||
id: 'tool_shapelib',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'shapes.png',
|
||||
type: 'mode_flyout', // _flyout
|
||||
position: 6,
|
||||
events: {
|
||||
click () {
|
||||
canv.setMode(modeId);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return {
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'ext-shapes.xml',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
callback () {
|
||||
$('<style>').text(`
|
||||
#shape_buttons {
|
||||
overflow: auto;
|
||||
width: 180px;
|
||||
max-height: 300px;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#shape_cats {
|
||||
min-width: 110px;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
height: 300px;
|
||||
}
|
||||
#shape_cats > div {
|
||||
line-height: 1em;
|
||||
padding: .5em;
|
||||
border:1px solid #B0B0B0;
|
||||
background: #E8E8E8;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
#shape_cats div:hover {
|
||||
background: #FFFFCC;
|
||||
}
|
||||
#shape_cats div.current {
|
||||
font-weight: bold;
|
||||
}
|
||||
`).appendTo('head');
|
||||
|
||||
const btnDiv = $('<div id="shape_buttons">');
|
||||
$('#tools_shapelib > *').wrapAll(btnDiv);
|
||||
|
||||
const shower = $('#tools_shapelib_show');
|
||||
|
||||
loadLibrary('basic');
|
||||
|
||||
// Do mouseup on parent element rather than each button
|
||||
$('#shape_buttons').mouseup(function (evt) {
|
||||
const btn = $(evt.target).closest('div.tool_button');
|
||||
|
||||
if (!btn.length) { return; }
|
||||
|
||||
const copy = btn.children().clone();
|
||||
shower.children(':not(.flyout_arrow_horiz)').remove();
|
||||
shower
|
||||
.append(copy)
|
||||
.attr('data-curopt', '#' + btn[0].id) // This sets the current mode
|
||||
.mouseup();
|
||||
canv.setMode(modeId);
|
||||
|
||||
curShapeId = btn[0].id.substr((modeId + '_').length);
|
||||
currentD = curLib.data[curShapeId];
|
||||
|
||||
$('.tools_flyout').fadeOut();
|
||||
});
|
||||
|
||||
const shapeCats = $('<div id="shape_cats">');
|
||||
|
||||
let catStr = '';
|
||||
$.each(categories, function (id, label) {
|
||||
catStr += '<div data-cat=' + id + '>' + label + '</div>';
|
||||
});
|
||||
|
||||
shapeCats.html(catStr).children().bind('mouseup', function () {
|
||||
const catlink = $(this);
|
||||
catlink.siblings().removeClass('current');
|
||||
catlink.addClass('current');
|
||||
|
||||
loadLibrary(catlink.attr('data-cat'));
|
||||
// Get stuff
|
||||
return false;
|
||||
});
|
||||
|
||||
shapeCats.children().eq(0).addClass('current');
|
||||
|
||||
$('#tools_shapelib').append(shapeCats);
|
||||
|
||||
shower.mouseup(function () {
|
||||
canv.setMode(currentD ? modeId : 'select');
|
||||
});
|
||||
$('#tool_shapelib').remove();
|
||||
|
||||
const h = $('#tools_shapelib').height();
|
||||
$('#tools_shapelib').css({
|
||||
'margin-top': -(h / 2 - 15),
|
||||
'margin-left': 3
|
||||
});
|
||||
// Now add shape categories from locale
|
||||
const cats = {};
|
||||
Object.entries(categories).forEach(([o, categoryName]) => {
|
||||
cats['#shape_cats [data-cat="' + o + '"]'] = categoryName;
|
||||
});
|
||||
this.setStrings('content', cats);
|
||||
},
|
||||
mouseDown (opts) {
|
||||
const mode = canv.getMode();
|
||||
if (mode !== modeId) { return undefined; }
|
||||
|
||||
startX = opts.start_x;
|
||||
const x = startX;
|
||||
startY = opts.start_y;
|
||||
const y = startY;
|
||||
const curStyle = canv.getStyle();
|
||||
|
||||
startClientPos.x = opts.event.clientX;
|
||||
startClientPos.y = opts.event.clientY;
|
||||
|
||||
curShape = canv.addSVGElementFromJson({
|
||||
element: 'path',
|
||||
curStyles: true,
|
||||
attr: {
|
||||
d: currentD,
|
||||
id: canv.getNextId(),
|
||||
opacity: curStyle.opacity / 2,
|
||||
style: 'pointer-events:none'
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure shape uses absolute values
|
||||
if ((/[a-z]/).test(currentD)) {
|
||||
currentD = curLib.data[curShapeId] = canv.pathActions.convertPath(curShape);
|
||||
curShape.setAttribute('d', currentD);
|
||||
canv.pathActions.fixEnd(curShape);
|
||||
}
|
||||
curShape.setAttribute('transform', 'translate(' + x + ',' + y + ') scale(0.005) translate(' + -x + ',' + -y + ')');
|
||||
|
||||
canv.recalculateDimensions(curShape);
|
||||
|
||||
/* const tlist = */ canv.getTransformList(curShape);
|
||||
|
||||
lastBBox = curShape.getBBox();
|
||||
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
},
|
||||
mouseMove (opts) {
|
||||
const mode = canv.getMode();
|
||||
if (mode !== modeId) { return; }
|
||||
|
||||
const zoom = canv.getZoom();
|
||||
const evt = opts.event;
|
||||
|
||||
const x = opts.mouse_x / zoom;
|
||||
const y = opts.mouse_y / zoom;
|
||||
|
||||
const tlist = canv.getTransformList(curShape),
|
||||
box = curShape.getBBox(),
|
||||
left = box.x, top = box.y;
|
||||
// {width, height} = box,
|
||||
// const dx = (x - startX), dy = (y - startY);
|
||||
|
||||
const newbox = {
|
||||
x: Math.min(startX, x),
|
||||
y: Math.min(startY, y),
|
||||
width: Math.abs(x - startX),
|
||||
height: Math.abs(y - startY)
|
||||
};
|
||||
|
||||
/*
|
||||
// This is currently serving no purpose, so commenting out
|
||||
let sy = height ? (height + dy) / height : 1,
|
||||
sx = width ? (width + dx) / width : 1;
|
||||
*/
|
||||
|
||||
let sx = (newbox.width / lastBBox.width) || 1;
|
||||
let sy = (newbox.height / lastBBox.height) || 1;
|
||||
|
||||
// Not perfect, but mostly works...
|
||||
let tx = 0;
|
||||
if (x < startX) {
|
||||
tx = lastBBox.width;
|
||||
}
|
||||
let ty = 0;
|
||||
if (y < startY) {
|
||||
ty = lastBBox.height;
|
||||
}
|
||||
|
||||
// update the transform list with translate,scale,translate
|
||||
const translateOrigin = svgroot.createSVGTransform(),
|
||||
scale = svgroot.createSVGTransform(),
|
||||
translateBack = svgroot.createSVGTransform();
|
||||
|
||||
translateOrigin.setTranslate(-(left + tx), -(top + ty));
|
||||
if (!evt.shiftKey) {
|
||||
const max = Math.min(Math.abs(sx), Math.abs(sy));
|
||||
|
||||
sx = max * (sx < 0 ? -1 : 1);
|
||||
sy = max * (sy < 0 ? -1 : 1);
|
||||
}
|
||||
scale.setScale(sx, sy);
|
||||
|
||||
translateBack.setTranslate(left + tx, top + ty);
|
||||
tlist.appendItem(translateBack);
|
||||
tlist.appendItem(scale);
|
||||
tlist.appendItem(translateOrigin);
|
||||
|
||||
canv.recalculateDimensions(curShape);
|
||||
|
||||
lastBBox = curShape.getBBox();
|
||||
},
|
||||
mouseUp (opts) {
|
||||
const mode = canv.getMode();
|
||||
if (mode !== modeId) { return undefined; }
|
||||
|
||||
const keepObject = (opts.event.clientX !== startClientPos.x && opts.event.clientY !== startClientPos.y);
|
||||
|
||||
return {
|
||||
keep: keepObject,
|
||||
element: curShape,
|
||||
started: false
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
10
src/editor/extensions/ext-shapes.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="tool_shapelib">
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||
<path fill="#c0c0c0" stroke-linejoin="round" stroke-width="14" stroke="#202020" fill-rule="nonzero" d="m70,194.72501l0,0c0,-10.30901 35.8172,-18.666 80,-18.666c44.18298,0 80,8.35699 80,18.666l0,74.66699c0,10.30899 -35.81702,18.66699 -80,18.66699c-44.1828,0 -80,-8.358 -80,-18.66699l0,-74.66699z"/>
|
||||
<path fill="#c0c0c0" stroke-linejoin="round" stroke-width="14" stroke="#202020" fill-rule="nonzero" d="m70,114.608l0,0c0,-10.309 35.8172,-18.6668 80,-18.6668c44.18298,0 80,8.3578 80,18.6668l0,74.66699c0,10.30901 -35.81702,18.666 -80,18.666c-44.1828,0 -80,-8.35699 -80,-18.666l0,-74.66699z"/>
|
||||
<path fill="#c0c0c0" stroke-linejoin="round" stroke-width="14" stroke="#202020" fill-rule="nonzero" d="m70,33.6667l0,0c0,-10.3094 35.8172,-18.6667 80,-18.6667c44.18298,0 80,8.3573 80,18.6667l0,74.6663c0,10.31 -35.81702,18.667 -80,18.667c-44.1828,0 -80,-8.357 -80,-18.667l0,-74.6663z"/>
|
||||
<path id="svg_1" fill="#c0c0c0" stroke-linejoin="round" stroke-width="14" stroke="#202020" fill-rule="nonzero" d="m230,32.33334c0,10.30931 -35.81726,18.66666 -80,18.66666c-44.1828,0 -80,-8.35735 -80,-18.66666"/>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
248
src/editor/extensions/ext-star.js
Normal file
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @file ext-star.js
|
||||
*
|
||||
*
|
||||
* @copyright 2010 CloudCanvas, Inc. All rights reserved
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
name: 'star',
|
||||
async init (S) {
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
|
||||
const {$, importLocale} = S; // {svgcontent},
|
||||
let
|
||||
selElems,
|
||||
// editingitex = false,
|
||||
// svgdoc = S.svgroot.parentNode.ownerDocument,
|
||||
started,
|
||||
newFO;
|
||||
// edg = 0,
|
||||
// newFOG, newFOGParent, newDef, newImageName, newMaskID,
|
||||
// undoCommand = 'Not image',
|
||||
// modeChangeG, ccZoom, wEl, hEl, wOffset, hOffset, ccRgbEl, brushW, brushH;
|
||||
const strings = await importLocale();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {boolean} on
|
||||
* @returns {void}
|
||||
*/
|
||||
function showPanel (on) {
|
||||
let fcRules = $('#fc_rules');
|
||||
if (!fcRules.length) {
|
||||
fcRules = $('<style id="fc_rules"></style>').appendTo('head');
|
||||
}
|
||||
fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }');
|
||||
$('#star_panel').toggle(on);
|
||||
}
|
||||
|
||||
/*
|
||||
function toggleSourceButtons(on){
|
||||
$('#star_save, #star_cancel').toggle(on);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} attr
|
||||
* @param {string|Float} val
|
||||
* @returns {void}
|
||||
*/
|
||||
function setAttr (attr, val) {
|
||||
svgCanvas.changeSelectedAttribute(attr, val);
|
||||
svgCanvas.call('changed', selElems);
|
||||
}
|
||||
|
||||
/*
|
||||
function cot(n){
|
||||
return 1 / Math.tan(n);
|
||||
}
|
||||
|
||||
function sec(n){
|
||||
return 1 / Math.cos(n);
|
||||
}
|
||||
*/
|
||||
const buttons = [{
|
||||
id: 'tool_star',
|
||||
icon: svgEditor.curConfig.extIconsPath + 'star.png',
|
||||
type: 'mode',
|
||||
position: 12,
|
||||
events: {
|
||||
click () {
|
||||
showPanel(true);
|
||||
svgCanvas.setMode('star');
|
||||
}
|
||||
}
|
||||
}];
|
||||
const contextTools = [{
|
||||
type: 'input',
|
||||
panel: 'star_panel',
|
||||
id: 'starNumPoints',
|
||||
size: 3,
|
||||
defval: 5,
|
||||
events: {
|
||||
change () {
|
||||
setAttr('point', this.value);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'input',
|
||||
panel: 'star_panel',
|
||||
id: 'starRadiusMulitplier',
|
||||
size: 3,
|
||||
defval: 2.5
|
||||
}, {
|
||||
type: 'input',
|
||||
panel: 'star_panel',
|
||||
id: 'radialShift',
|
||||
size: 3,
|
||||
defval: 0,
|
||||
events: {
|
||||
change () {
|
||||
setAttr('radialshift', this.value);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'star-icons.svg',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
}),
|
||||
context_tools: strings.contextTools.map((contextTool, i) => {
|
||||
return Object.assign(contextTools[i], contextTool);
|
||||
}),
|
||||
callback () {
|
||||
$('#star_panel').hide();
|
||||
// const endChanges = function(){};
|
||||
},
|
||||
mouseDown (opts) {
|
||||
const rgb = svgCanvas.getColor('fill');
|
||||
// const ccRgbEl = rgb.substring(1, rgb.length);
|
||||
const sRgb = svgCanvas.getColor('stroke');
|
||||
// const ccSRgbEl = sRgb.substring(1, rgb.length);
|
||||
const sWidth = svgCanvas.getStrokeWidth();
|
||||
|
||||
if (svgCanvas.getMode() === 'star') {
|
||||
started = true;
|
||||
|
||||
newFO = svgCanvas.addSVGElementFromJson({
|
||||
element: 'polygon',
|
||||
attr: {
|
||||
cx: opts.start_x,
|
||||
cy: opts.start_y,
|
||||
id: svgCanvas.getNextId(),
|
||||
shape: 'star',
|
||||
point: document.getElementById('starNumPoints').value,
|
||||
r: 0,
|
||||
radialshift: document.getElementById('radialShift').value,
|
||||
r2: 0,
|
||||
orient: 'point',
|
||||
fill: rgb,
|
||||
strokecolor: sRgb,
|
||||
strokeWidth: sWidth
|
||||
}
|
||||
});
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
mouseMove (opts) {
|
||||
if (!started) {
|
||||
return undefined;
|
||||
}
|
||||
if (svgCanvas.getMode() === 'star') {
|
||||
const c = $(newFO).attr(['cx', 'cy', 'point', 'orient', 'fill', 'strokecolor', 'strokeWidth', 'radialshift']);
|
||||
|
||||
let x = opts.mouse_x;
|
||||
let y = opts.mouse_y;
|
||||
const {cx, cy, fill, strokecolor, strokeWidth, radialshift, point, orient} = c,
|
||||
circumradius = (Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))) / 1.5,
|
||||
inradius = circumradius / document.getElementById('starRadiusMulitplier').value;
|
||||
newFO.setAttribute('r', circumradius);
|
||||
newFO.setAttribute('r2', inradius);
|
||||
|
||||
let polyPoints = '';
|
||||
for (let s = 0; point >= s; s++) {
|
||||
let angle = 2.0 * Math.PI * (s / point);
|
||||
if (orient === 'point') {
|
||||
angle -= (Math.PI / 2);
|
||||
} else if (orient === 'edge') {
|
||||
angle = (angle + (Math.PI / point)) - (Math.PI / 2);
|
||||
}
|
||||
|
||||
x = (circumradius * Math.cos(angle)) + cx;
|
||||
y = (circumradius * Math.sin(angle)) + cy;
|
||||
|
||||
polyPoints += x + ',' + y + ' ';
|
||||
|
||||
if (!isNaN(inradius)) {
|
||||
angle = (2.0 * Math.PI * (s / point)) + (Math.PI / point);
|
||||
if (orient === 'point') {
|
||||
angle -= (Math.PI / 2);
|
||||
} else if (orient === 'edge') {
|
||||
angle = (angle + (Math.PI / point)) - (Math.PI / 2);
|
||||
}
|
||||
angle += radialshift;
|
||||
|
||||
x = (inradius * Math.cos(angle)) + cx;
|
||||
y = (inradius * Math.sin(angle)) + cy;
|
||||
|
||||
polyPoints += x + ',' + y + ' ';
|
||||
}
|
||||
}
|
||||
newFO.setAttribute('points', polyPoints);
|
||||
newFO.setAttribute('fill', fill);
|
||||
newFO.setAttribute('stroke', strokecolor);
|
||||
newFO.setAttribute('stroke-width', strokeWidth);
|
||||
/* const shape = */ newFO.getAttribute('shape');
|
||||
|
||||
return {
|
||||
started: true
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
mouseUp () {
|
||||
if (svgCanvas.getMode() === 'star') {
|
||||
const attrs = $(newFO).attr(['r']);
|
||||
// svgCanvas.addToSelection([newFO], true);
|
||||
return {
|
||||
keep: (attrs.r !== '0'),
|
||||
element: newFO
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
selectedChanged (opts) {
|
||||
// Use this to update the current selected elements
|
||||
selElems = opts.elems;
|
||||
|
||||
let i = selElems.length;
|
||||
while (i--) {
|
||||
const elem = selElems[i];
|
||||
if (elem && elem.getAttribute('shape') === 'star') {
|
||||
if (opts.selectedElement && !opts.multiselected) {
|
||||
// $('#starRadiusMulitplier').val(elem.getAttribute('r2'));
|
||||
$('#starNumPoints').val(elem.getAttribute('point'));
|
||||
$('#radialShift').val(elem.getAttribute('radialshift'));
|
||||
showPanel(true);
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
} else {
|
||||
showPanel(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
elementChanged (opts) {
|
||||
// const elem = opts.elems[0];
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
298
src/editor/extensions/ext-storage.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* @file ext-storage.js
|
||||
*
|
||||
* This extension allows automatic saving of the SVG canvas contents upon
|
||||
* page unload (which can later be automatically retrieved upon future
|
||||
* editor loads).
|
||||
*
|
||||
* The functionality was originally part of the SVG Editor, but moved to a
|
||||
* separate extension to make the setting behavior optional, and adapted
|
||||
* to inform the user of its setting of local data.
|
||||
*
|
||||
* @license MIT
|
||||
*
|
||||
* @copyright 2010 Brett Zamir
|
||||
* @todo Revisit on whether to use `svgEditor.pref` over directly setting
|
||||
* `curConfig` in all extensions for a more public API (not only for `extPath`
|
||||
* and `imagePath`, but other currently used config in the extensions)
|
||||
* @todo We might provide control of storage settings through the UI besides the
|
||||
* initial (or URL-forced) dialog. *
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'storage',
|
||||
init ({$}) {
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
|
||||
// We could empty any already-set data for users when they decline storage,
|
||||
// but it would be a risk for users who wanted to store but accidentally
|
||||
// said "no"; instead, we'll let those who already set it, delete it themselves;
|
||||
// to change, set the "emptyStorageOnDecline" config setting to true
|
||||
// in svgedit-config-iife.js/svgedit-config-es.js.
|
||||
const {
|
||||
emptyStorageOnDecline,
|
||||
// When the code in svg-editor.js prevents local storage on load per
|
||||
// user request, we also prevent storing on unload here so as to
|
||||
// avoid third-party sites making XSRF requests or providing links
|
||||
// which would cause the user's local storage not to load and then
|
||||
// upon page unload (such as the user closing the window), the storage
|
||||
// would thereby be set with an empty value, erasing any of the
|
||||
// user's prior work. To change this behavior so that no use of storage
|
||||
// or adding of new storage takes place regardless of settings, set
|
||||
// the "noStorageOnLoad" config setting to true in svgedit-config-*.js.
|
||||
noStorageOnLoad,
|
||||
forceStorage
|
||||
} = svgEditor.curConfig;
|
||||
const {storage, updateCanvas} = svgEditor;
|
||||
|
||||
/**
|
||||
* Replace `storagePrompt` parameter within URL.
|
||||
* @param {string} val
|
||||
* @returns {void}
|
||||
* @todo Replace the string manipulation with `searchParams.set`
|
||||
*/
|
||||
function replaceStoragePrompt (val) {
|
||||
val = val ? 'storagePrompt=' + val : '';
|
||||
const loc = top.location; // Allow this to work with the embedded editor as well
|
||||
if (loc.href.includes('storagePrompt=')) {
|
||||
/*
|
||||
loc.href = loc.href.replace(/(?<sep>[&?])storagePrompt=[^&]*(?<amp>&?)/, function (n0, sep, amp) {
|
||||
return (val ? sep : '') + val + (!val && amp ? sep : (amp || ''));
|
||||
});
|
||||
*/
|
||||
loc.href = loc.href.replace(/([&?])storagePrompt=[^&]*(&?)/, function (n0, n1, amp) {
|
||||
return (val ? n1 : '') + val + (!val && amp ? n1 : (amp || ''));
|
||||
});
|
||||
} else {
|
||||
loc.href += (loc.href.includes('?') ? '&' : '?') + val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets SVG content as a string with "svgedit-" and the current
|
||||
* canvas name as namespace.
|
||||
* @param {string} val
|
||||
* @returns {void}
|
||||
*/
|
||||
function setSVGContentStorage (val) {
|
||||
if (storage) {
|
||||
const name = 'svgedit-' + svgEditor.curConfig.canvasName;
|
||||
if (!val) {
|
||||
storage.removeItem(name);
|
||||
} else {
|
||||
storage.setItem(name, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cookie to expire.
|
||||
* @param {string} cookie
|
||||
* @returns {void}
|
||||
*/
|
||||
function expireCookie (cookie) {
|
||||
document.cookie = encodeURIComponent(cookie) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire the storage cookie.
|
||||
* @returns {void}
|
||||
*/
|
||||
function removeStoragePrefCookie () {
|
||||
expireCookie('svgeditstore');
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties storage for each of the current preferences.
|
||||
* @returns {void}
|
||||
*/
|
||||
function emptyStorage () {
|
||||
setSVGContentStorage('');
|
||||
Object.keys(svgEditor.curPrefs).forEach((name) => {
|
||||
name = 'svg-edit-' + name;
|
||||
if (storage) {
|
||||
storage.removeItem(name);
|
||||
}
|
||||
expireCookie(name);
|
||||
});
|
||||
}
|
||||
|
||||
// emptyStorage();
|
||||
|
||||
/**
|
||||
* Listen for unloading: If and only if opted in by the user, set the content
|
||||
* document and preferences into storage:
|
||||
* 1. Prevent save warnings (since we're automatically saving unsaved
|
||||
* content into storage)
|
||||
* 2. Use localStorage to set SVG contents (potentially too large to allow in cookies)
|
||||
* 3. Use localStorage (where available) or cookies to set preferences.
|
||||
* @returns {void}
|
||||
*/
|
||||
function setupBeforeUnloadListener () {
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
// Don't save anything unless the user opted in to storage
|
||||
if (!document.cookie.match(/(?:^|;\s*)svgeditstore=(?:prefsAndContent|prefsOnly)/)) {
|
||||
return;
|
||||
}
|
||||
if (document.cookie.match(/(?:^|;\s*)svgeditstore=prefsAndContent/)) {
|
||||
setSVGContentStorage(svgCanvas.getSvgString());
|
||||
}
|
||||
|
||||
svgEditor.setConfig({no_save_warning: true}); // No need for explicit saving at all once storage is on
|
||||
// svgEditor.showSaveWarning = false;
|
||||
|
||||
const {curPrefs} = svgEditor;
|
||||
|
||||
Object.entries(curPrefs).forEach(([key, val]) => {
|
||||
const store = (val !== undefined);
|
||||
key = 'svg-edit-' + key;
|
||||
if (!store) {
|
||||
return;
|
||||
}
|
||||
if (storage) {
|
||||
storage.setItem(key, val);
|
||||
} else if (window.widget) {
|
||||
window.widget.setPreferenceForKey(val, key);
|
||||
} else {
|
||||
val = encodeURIComponent(val);
|
||||
document.cookie = encodeURIComponent(key) + '=' + val + '; expires=Fri, 31 Dec 9999 23:59:59 GMT';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let loaded = false;
|
||||
return {
|
||||
name: 'storage',
|
||||
async langReady ({importLocale}) {
|
||||
const storagePrompt = new URL(top.location).searchParams.get('storagePrompt');
|
||||
|
||||
const confirmSetStorage = await importLocale();
|
||||
const {
|
||||
message, storagePrefsAndContent, storagePrefsOnly,
|
||||
storagePrefs, storageNoPrefsOrContent, storageNoPrefs,
|
||||
rememberLabel, rememberTooltip
|
||||
} = confirmSetStorage;
|
||||
|
||||
// No need to run this one-time dialog again just because the user
|
||||
// changes the language
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
loaded = true;
|
||||
|
||||
// Note that the following can load even if "noStorageOnLoad" is
|
||||
// set to false; to avoid any chance of storage, avoid this
|
||||
// extension! (and to avoid using any prior storage, set the
|
||||
// config option "noStorageOnLoad" to true).
|
||||
if (!forceStorage && (
|
||||
// If the URL has been explicitly set to always prompt the
|
||||
// user (e.g., so one can be pointed to a URL where one
|
||||
// can alter one's settings, say to prevent future storage)...
|
||||
storagePrompt === 'true' ||
|
||||
(
|
||||
// ...or...if the URL at least doesn't explicitly prevent a
|
||||
// storage prompt (as we use for users who
|
||||
// don't want to set cookies at all but who don't want
|
||||
// continual prompts about it)...
|
||||
storagePrompt !== 'false' &&
|
||||
// ...and this user hasn't previously indicated a desire for storage
|
||||
!document.cookie.match(/(?:^|;\s*)svgeditstore=(?:prefsAndContent|prefsOnly)/)
|
||||
)
|
||||
// ...then show the storage prompt.
|
||||
)) {
|
||||
const options = [];
|
||||
if (storage) {
|
||||
options.unshift(
|
||||
{value: 'prefsAndContent', text: storagePrefsAndContent},
|
||||
{value: 'prefsOnly', text: storagePrefsOnly},
|
||||
{value: 'noPrefsOrContent', text: storageNoPrefsOrContent}
|
||||
);
|
||||
} else {
|
||||
options.unshift(
|
||||
{value: 'prefsOnly', text: storagePrefs},
|
||||
{value: 'noPrefsOrContent', text: storageNoPrefs}
|
||||
);
|
||||
}
|
||||
|
||||
// Hack to temporarily provide a wide and high enough dialog
|
||||
const oldContainerWidth = $('#dialog_container')[0].style.width,
|
||||
oldContainerMarginLeft = $('#dialog_container')[0].style.marginLeft,
|
||||
oldContentHeight = $('#dialog_content')[0].style.height,
|
||||
oldContainerHeight = $('#dialog_container')[0].style.height;
|
||||
$('#dialog_content')[0].style.height = '120px';
|
||||
$('#dialog_container')[0].style.height = '170px';
|
||||
$('#dialog_container')[0].style.width = '800px';
|
||||
$('#dialog_container')[0].style.marginLeft = '-400px';
|
||||
|
||||
// Open select-with-checkbox dialog
|
||||
// From svg-editor.js
|
||||
svgEditor.storagePromptState = 'waiting';
|
||||
const {response: pref, checked} = await $.select(
|
||||
message,
|
||||
options,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
label: rememberLabel,
|
||||
checked: true,
|
||||
tooltip: rememberTooltip
|
||||
}
|
||||
);
|
||||
if (pref && pref !== 'noPrefsOrContent') {
|
||||
// Regardless of whether the user opted
|
||||
// to remember the choice (and move to a URL which won't
|
||||
// ask them again), we have to assume the user
|
||||
// doesn't even want to remember their not wanting
|
||||
// storage, so we don't set the cookie or continue on with
|
||||
// setting storage on beforeunload
|
||||
document.cookie = 'svgeditstore=' + encodeURIComponent(pref) + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; // 'prefsAndContent' | 'prefsOnly'
|
||||
// If the URL was configured to always insist on a prompt, if
|
||||
// the user does indicate a wish to store their info, we
|
||||
// don't want ask them again upon page refresh so move
|
||||
// them instead to a URL which does not always prompt
|
||||
if (storagePrompt === 'true' && checked) {
|
||||
replaceStoragePrompt();
|
||||
return;
|
||||
}
|
||||
} else { // The user does not wish storage (or cancelled, which we treat equivalently)
|
||||
removeStoragePrefCookie();
|
||||
if (pref && // If the user explicitly expresses wish for no storage
|
||||
emptyStorageOnDecline
|
||||
) {
|
||||
emptyStorage();
|
||||
}
|
||||
if (pref && checked) {
|
||||
// Open a URL which won't set storage and won't prompt user about storage
|
||||
replaceStoragePrompt('false');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset width/height of dialog (e.g., for use by Export)
|
||||
$('#dialog_container')[0].style.width = oldContainerWidth;
|
||||
$('#dialog_container')[0].style.marginLeft = oldContainerMarginLeft;
|
||||
$('#dialog_content')[0].style.height = oldContentHeight;
|
||||
$('#dialog_container')[0].style.height = oldContainerHeight;
|
||||
|
||||
// It should be enough to (conditionally) add to storage on
|
||||
// beforeunload, but if we wished to update immediately,
|
||||
// we might wish to try setting:
|
||||
// svgEditor.setConfig({noStorageOnLoad: true});
|
||||
// and then call:
|
||||
// svgEditor.loadContentAndPrefs();
|
||||
|
||||
// We don't check for noStorageOnLoad here because
|
||||
// the prompt gives the user the option to store data
|
||||
setupBeforeUnloadListener();
|
||||
|
||||
svgEditor.storagePromptState = 'closed';
|
||||
updateCanvas(true);
|
||||
} else if (!noStorageOnLoad || forceStorage) {
|
||||
setupBeforeUnloadListener();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
109
src/editor/extensions/ext-webappfind.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Depends on Firefox add-on and executables from
|
||||
* {@link https://github.com/brettz9/webappfind}.
|
||||
* @author Brett Zamir
|
||||
* @license MIT
|
||||
* @todo See WebAppFind Readme for SVG-related todos
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'webappfind',
|
||||
async init ({importLocale, $}) {
|
||||
const strings = await importLocale();
|
||||
const svgEditor = this;
|
||||
const saveMessage = 'save',
|
||||
readMessage = 'read',
|
||||
excludedMessages = [readMessage, saveMessage];
|
||||
|
||||
let pathID;
|
||||
this.canvas.bind(
|
||||
'message',
|
||||
/**
|
||||
* @param {external:Window} win
|
||||
* @param {PlainObject} info
|
||||
* @param {module:svgcanvas.SvgCanvas#event:message} info.data
|
||||
* @param {string} info.origin
|
||||
* @listens module:svgcanvas.SvgCanvas#event:message
|
||||
* @throws {Error} Unexpected event type
|
||||
* @returns {void}
|
||||
*/
|
||||
(win, {data, origin}) => { // eslint-disable-line no-shadow
|
||||
// console.log('data, origin', data, origin);
|
||||
let type, content;
|
||||
try {
|
||||
({type, pathID, content} = data.webappfind); // May throw if data is not an object
|
||||
if (origin !== location.origin || // We are only interested in a message sent as though within this URL by our browser add-on
|
||||
excludedMessages.includes(type) // Avoid our post below (other messages might be possible in the future which may also need to be excluded if your subsequent code makes assumptions on the type of message this is)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'view':
|
||||
// Populate the contents
|
||||
svgEditor.loadFromString(content);
|
||||
|
||||
/* if ($('#tool_save_file')) {
|
||||
$('#tool_save_file').disabled = false;
|
||||
} */
|
||||
break;
|
||||
case 'save-end':
|
||||
$.alert(`save complete for pathID ${pathID}!`);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unexpected WebAppFind event type');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
window.postMessage({
|
||||
webappfind: {
|
||||
type: readMessage
|
||||
}
|
||||
}, window.location.origin === 'null'
|
||||
// Avoid "null" string error for `file:` protocol (even though
|
||||
// file protocol not currently supported by Firefox)
|
||||
? '*'
|
||||
: window.location.origin
|
||||
);
|
||||
*/
|
||||
const buttons = [{
|
||||
id: 'webappfind_save', //
|
||||
icon: svgEditor.curConfig.extIconsPath + 'webappfind.png',
|
||||
type: 'app_menu',
|
||||
position: 4, // Before 0-based index position 4 (after the regular "Save Image (S)")
|
||||
events: {
|
||||
click () {
|
||||
if (!pathID) { // Not ready yet as haven't received first payload
|
||||
return;
|
||||
}
|
||||
window.postMessage(
|
||||
{
|
||||
webappfind: {
|
||||
type: saveMessage,
|
||||
pathID,
|
||||
content: svgEditor.canvas.getSvgString()
|
||||
}
|
||||
}, window.location.origin === 'null'
|
||||
// Avoid "null" string error for `file:` protocol (even
|
||||
// though file protocol not currently supported by add-on)
|
||||
? '*'
|
||||
: window.location.origin
|
||||
);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return {
|
||||
name: strings.name,
|
||||
svgicons: svgEditor.curConfig.extIconsPath + 'webappfind-icon.svg',
|
||||
buttons: strings.buttons.map((button, i) => {
|
||||
return Object.assign(buttons[i], button);
|
||||
})
|
||||
};
|
||||
}
|
||||
};
|
||||
48
src/editor/extensions/ext-xdomain-messaging.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Should not be needed for same domain control (just call via child frame),
|
||||
* but an API common for cross-domain and same domain use can be found
|
||||
* in embedapi.js with a demo at embedapi.html.
|
||||
*/
|
||||
export default {
|
||||
name: 'xdomain-messaging',
|
||||
init () {
|
||||
const svgEditor = this;
|
||||
const svgCanvas = svgEditor.canvas;
|
||||
try {
|
||||
window.addEventListener('message', function (e) {
|
||||
// We accept and post strings for the sake of IE9 support
|
||||
if (!e.data || !['string', 'object'].includes(typeof e.data) || e.data.charAt() === '|') {
|
||||
return;
|
||||
}
|
||||
const data = typeof e.data === 'object' ? e.data : JSON.parse(e.data);
|
||||
if (!data || typeof data !== 'object' || data.namespace !== 'svgCanvas') {
|
||||
return;
|
||||
}
|
||||
// The default is not to allow any origins, including even the same domain or
|
||||
// if run on a `file:///` URL. See `svgedit-config-es.js` for an example of how
|
||||
// to configure
|
||||
const {allowedOrigins} = svgEditor.curConfig;
|
||||
if (!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin)) {
|
||||
console.log(`Origin ${e.origin} not whitelisted for posting to ${window.origin}`); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
const cbid = data.id;
|
||||
const {name, args} = data;
|
||||
const message = {
|
||||
namespace: 'svg-edit',
|
||||
id: cbid
|
||||
};
|
||||
try {
|
||||
// Now that we know the origin is trusted, we perform otherwise
|
||||
// unsafe arbitrary canvas method execution
|
||||
message.result = svgCanvas[name](...args); // lgtm [js/remote-property-injection]
|
||||
} catch (err) {
|
||||
message.error = err.message;
|
||||
}
|
||||
e.source.postMessage(JSON.stringify(message), '*');
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error with xdomain message listener: ' + err); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
};
|
||||
34
src/editor/extensions/eyedropper-icon.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="tool_eyedropper">
|
||||
<svg viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit -->
|
||||
<defs>
|
||||
<radialGradient id="eyedropper_svg_6" cx="0.5" cy="0.5" r="0.5">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#e5e5e5" stop-opacity="0.38"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="eyedropper_svg_15" x1="0" y1="0" x2="0.58594" y2="0.55078">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.57"/>
|
||||
<stop offset="1" stop-color="#000056" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="eyedropper_svg_19" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g display="inline">
|
||||
<title>Layer 1</title>
|
||||
<path d="m193.899994,73l-119.899979,118l-15,39.5l10.25,4.5l43.750015,-20l108.999969,-112l-28.100006,-30z" id="svg_3" fill="none" stroke="#000000" stroke-width="5"/>
|
||||
<path d="m58.649994,232c-2.75,28.200012 -26.399994,28.950012 -21.899994,59c4.5,30.049988 55,28 55.5,-1.25c0.5,-29.25 -20.25,-28.75 -22.25,-54.75l-11.350006,-3z" id="svg_4" fill="#aa56ff" stroke="#000000" stroke-width="7"/>
|
||||
<path d="m45.474976,269.275024l13.775024,0.474976l-0.75,16.75l-14.25,-1.25l1.224976,-15.974976z" id="svg_5" fill="url(#eyedropper_svg_6)" stroke-width="5" fill-opacity="0.73"/>
|
||||
<path d="m217.899994,46c21.5,-101.549999 141.600006,20.449997 28.100006,33l-5,44l-63,-66l39.899994,-11z" id="svg_2" fill="#000000" stroke-width="5"/>
|
||||
<path d="m206.825012,61.075008c3.712494,-2.46249 7.637482,-3.53751 14.424988,-5.575008c10.125,-16.5 32.875,-41.5 40.5,-35c7.625,6.5 -21.25,35.625 -37.5,39.25c-5.5,10.125 -8,13.875 -17.25,16.5c-2.837494,-8.162514 -4.262482,-12.337486 -0.174988,-15.174992z" id="svg_7" fill="url(#eyedropper_svg_15)" stroke-width="5"/>
|
||||
<path d="m133.049988,134.75l46.950012,9.25l-66,70l-42.5,20.5l-11.5,-5l14,-37.5l59.049988,-57.25z" id="svg_11" fill="#aa56ff" stroke="#000000" stroke-width="7"/>
|
||||
<path d="m71.425034,212.350006l9.050888,-20.022537l51.516724,-49.327469l8.507355,0.97197l-69.074966,68.378036z" id="svg_16" fill="url(#eyedropper_svg_19)" stroke-width="5"/>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
|
||||
<g id="svg_eof"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/editor/extensions/eyedropper.png
Normal file
|
After Width: | Height: | Size: 568 B |
58
src/editor/extensions/fileopen.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<?php
|
||||
/*
|
||||
* fileopen.php
|
||||
* To be used with ext-server_opensave.js for SVG-edit
|
||||
*
|
||||
* Licensed under the MIT License
|
||||
*
|
||||
* Copyright(c) 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
// Very minimal PHP file, all we do is Base64 encode the uploaded file and
|
||||
// return it to the editor
|
||||
|
||||
if (!isset($_REQUEST['type'])) {
|
||||
echo 'No type given';
|
||||
exit;
|
||||
}
|
||||
$type = $_REQUEST['type'];
|
||||
if (!in_array($type, array('load_svg', 'import_svg', 'import_img'))) {
|
||||
echo 'Not a recognized type';
|
||||
exit;
|
||||
}
|
||||
|
||||
require('allowedMimeTypes.php');
|
||||
|
||||
$file = $_FILES['svg_file']['tmp_name'];
|
||||
|
||||
$output = file_get_contents($file);
|
||||
|
||||
$prefix = '';
|
||||
|
||||
// Make Data URL prefix for import image
|
||||
if ($type == 'import_img') {
|
||||
$info = getimagesize($file);
|
||||
if (!in_array($info['mime'], $allowedMimeTypesBySuffix)) {
|
||||
echo 'Disallowed MIME for supplied file';
|
||||
exit;
|
||||
}
|
||||
$prefix = 'data:' . $info['mime'] . ';base64,';
|
||||
}
|
||||
?>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>-</title>
|
||||
<script>
|
||||
|
||||
top.svgEditor.processFile("<?php
|
||||
|
||||
// This should be safe since SVG edit does its own filtering (e.g., if an SVG file contains scripts)
|
||||
echo $prefix . base64_encode($output);
|
||||
|
||||
?>", "<?php echo $type; ?>");
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
60
src/editor/extensions/filesave.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/*
|
||||
* filesave.php
|
||||
* To be used with ext-server_opensave.js for SVG-edit
|
||||
*
|
||||
* Licensed under the MIT License
|
||||
*
|
||||
* Copyright(c) 2010 Alexis Deveria
|
||||
*
|
||||
*/
|
||||
|
||||
function encodeRFC5987ValueChars ($str) {
|
||||
// See https://tools.ietf.org/html/rfc5987#section-3.2.1
|
||||
// For better readability within headers, add back the characters escaped by rawurlencode but still allowable
|
||||
// Although RFC3986 reserves "!" (%21), RFC5987 does not
|
||||
return preg_replace_callback('@%(2[1346B]|5E|60|7C)@', function ($matches) {
|
||||
return chr('0x' . $matches[1]);
|
||||
}, rawurlencode($str));
|
||||
}
|
||||
|
||||
require('allowedMimeTypes.php');
|
||||
|
||||
$mime = (!isset($_POST['mime']) || !in_array($_POST['mime'], $allowedMimeTypesBySuffix)) ? 'image/svg+xml;charset=UTF-8' : $_POST['mime'];
|
||||
|
||||
if (!isset($_POST['output_svg']) && !isset($_POST['output_img'])) {
|
||||
die('post fail');
|
||||
}
|
||||
|
||||
$file = '';
|
||||
|
||||
$suffix = '.' . array_search($mime, $allowedMimeTypesBySuffix);
|
||||
|
||||
if (isset($_POST['filename']) && strlen($_POST['filename']) > 0) {
|
||||
$file = $_POST['filename'] . $suffix;
|
||||
} else {
|
||||
$file = 'image' . $suffix;
|
||||
}
|
||||
|
||||
if ($suffix == '.svg') {
|
||||
$contents = $_POST['output_svg'];
|
||||
} else {
|
||||
$contents = $_POST['output_img'];
|
||||
$pos = (strpos($contents, 'base64,') + 7);
|
||||
$contents = base64_decode(substr($contents, $pos));
|
||||
}
|
||||
|
||||
header('Cache-Control: public');
|
||||
header('Content-Description: File Transfer');
|
||||
|
||||
// See https://tools.ietf.org/html/rfc6266#section-4.1
|
||||
header("Content-Disposition: attachment; filename*=UTF-8''" . encodeRFC5987ValueChars(
|
||||
// preg_replace('@[\\\\/:*?"<>|]@', '', $file) // If we wanted to strip Windows-disallowed characters server-side (but not a security issue, so we can strip client-side instead)
|
||||
$file
|
||||
));
|
||||
header('Content-Type: ' . $mime);
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
|
||||
echo $contents;
|
||||
|
||||
?>
|
||||
BIN
src/editor/extensions/foreignobject-edit.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
96
src/editor/extensions/foreignobject-icons.xml
Normal file
@@ -0,0 +1,96 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="tool_foreign">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 84 84">
|
||||
<g fill="#444" opacity="0.2" transform="translate(6,6)">
|
||||
<path d="M42.8,74.3c0,4.3,0,5.9,11.8,5.9l4.1,0l0,3.8c-4.5-0.4-16.1-0.4-21.2-0.3c-5.1,0-16.6,0-21,0.4l0-3.8l4.1,0
|
||||
c11.8,0,11.8-1.7,11.8-5.9l0-6.9C13.9,65.6,0,54.6,0,42c0-12.2,13.3-23.5,32.4-25.4l0-6.9c0-4.3,0-5.9-11.8-5.9l-4.1,0l0-3.8
|
||||
c4.5,0.4,16.1,0.4,21.2,0.3c5.1,0,16.6,0,21-0.4l0,3.8l-4.1,0c-11.8,0-11.8,1.7-11.8,5.9l0,6.9C61.6,18.1,75.8,29.2,75.8,42
|
||||
c0,12.4-13.8,23.9-33.1,25.4L42.8,74.3z M32.4,19.4c-18.7,2.5-19.9,16.2-19.9,22.6c0,7.6,2.3,20.2,20,22.5L32.4,19.4z M42.7,64.7
|
||||
c18.8-2.2,20.7-15.4,20.6-22.8c0-9.3-3.5-20.6-20.7-22.6L42.7,64.7z"/>
|
||||
</g>
|
||||
<g fill="#444" opacity="0.3" transform="translate(4,4)">
|
||||
<path d="M42.8,74.3c0,4.3,0,5.9,11.8,5.9l4.1,0l0,3.8c-4.5-0.4-16.1-0.4-21.2-0.3c-5.1,0-16.6,0-21,0.4l0-3.8l4.1,0
|
||||
c11.8,0,11.8-1.7,11.8-5.9l0-6.9C13.9,65.6,0,54.6,0,42c0-12.2,13.3-23.5,32.4-25.4l0-6.9c0-4.3,0-5.9-11.8-5.9l-4.1,0l0-3.8
|
||||
c4.5,0.4,16.1,0.4,21.2,0.3c5.1,0,16.6,0,21-0.4l0,3.8l-4.1,0c-11.8,0-11.8,1.7-11.8,5.9l0,6.9C61.6,18.1,75.8,29.2,75.8,42
|
||||
c0,12.4-13.8,23.9-33.1,25.4L42.8,74.3z M32.4,19.4c-18.7,2.5-19.9,16.2-19.9,22.6c0,7.6,2.3,20.2,20,22.5L32.4,19.4z M42.7,64.7
|
||||
c18.8-2.2,20.7-15.4,20.6-22.8c0-9.3-3.5-20.6-20.7-22.6L42.7,64.7z"/>
|
||||
</g>
|
||||
<g fill="#444" opacity="0.5" transform="translate(2,2)">
|
||||
<path d="M42.8,74.3c0,4.3,0,5.9,11.8,5.9l4.1,0l0,3.8c-4.5-0.4-16.1-0.4-21.2-0.3c-5.1,0-16.6,0-21,0.4l0-3.8l4.1,0
|
||||
c11.8,0,11.8-1.7,11.8-5.9l0-6.9C13.9,65.6,0,54.6,0,42c0-12.2,13.3-23.5,32.4-25.4l0-6.9c0-4.3,0-5.9-11.8-5.9l-4.1,0l0-3.8
|
||||
c4.5,0.4,16.1,0.4,21.2,0.3c5.1,0,16.6,0,21-0.4l0,3.8l-4.1,0c-11.8,0-11.8,1.7-11.8,5.9l0,6.9C61.6,18.1,75.8,29.2,75.8,42
|
||||
c0,12.4-13.8,23.9-33.1,25.4L42.8,74.3z M32.4,19.4c-18.7,2.5-19.9,16.2-19.9,22.6c0,7.6,2.3,20.2,20,22.5L32.4,19.4z M42.7,64.7
|
||||
c18.8-2.2,20.7-15.4,20.6-22.8c0-9.3-3.5-20.6-20.7-22.6L42.7,64.7z"/>
|
||||
</g>
|
||||
<g fill="#0000CC">
|
||||
<path id="xyz321" d="M42.8,74.3c0,4.3,0,5.9,11.8,5.9l4.1,0l0,3.8c-4.5-0.4-16.1-0.4-21.2-0.3c-5.1,0-16.6,0-21,0.4l0-3.8l4.1,0
|
||||
c11.8,0,11.8-1.7,11.8-5.9l0-6.9C13.9,65.6,0,54.6,0,42c0-12.2,13.3-23.5,32.4-25.4l0-6.9c0-4.3,0-5.9-11.8-5.9l-4.1,0l0-3.8
|
||||
c4.5,0.4,16.1,0.4,21.2,0.3c5.1,0,16.6,0,21-0.4l0,3.8l-4.1,0c-11.8,0-11.8,1.7-11.8,5.9l0,6.9C61.6,18.1,75.8,29.2,75.8,42
|
||||
c0,12.4-13.8,23.9-33.1,25.4L42.8,74.3z M32.4,19.4c-18.7,2.5-19.9,16.2-19.9,22.6c0,7.6,2.3,20.2,20,22.5L32.4,19.4z M42.7,64.7
|
||||
c18.8-2.2,20.7-15.4,20.6-22.8c0-9.3-3.5-20.6-20.7-22.6L42.7,64.7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
|
||||
<g id="edit_foreign">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="34 38 170 170" overflow="hidden">
|
||||
<g fill="#000088">
|
||||
<path d="M30.1,63.9v-4.3l30.2-14.9V50L36.5,61.7l23.8,11.7v5.3L30.1,63.9z"/>
|
||||
<path d="M106.1,79.7v-1.1c4.2-0.5,4.8-1.1,4.8-5.2V58.2c0-6-1.3-7.9-5.4-7.9c-3.3,0-5.7,1.3-7.8,4.4v18.1
|
||||
c0,4.5,1.1,5.7,5.2,5.8v1.1H86.8v-1.1c4.1-0.3,4.9-1.1,4.9-5.1V57.9c0-5-1.6-7.6-4.8-7.6c-2.5,0-5.6,1.2-7.4,2.9
|
||||
c-0.5,0.5-1.1,1.4-1.1,1.4v20.3c0,2.8,1.1,3.6,4.9,3.7v1.1h-16v-1.1c4-0.1,5-1.2,5-5V55.4c0-3.5-0.6-4.6-2.5-4.6
|
||||
c-0.8,0-1.4,0.1-2.3,0.3v-1.2c4-1.1,6.4-1.9,10.1-3.2l0.5,0.1v5.4c6-4.5,8-5.5,11.2-5.5c3.9,0,6.3,1.9,7.6,6c3.9-4.2,7.6-6,11.7-6
|
||||
c5.5,0,8.4,4.3,8.4,12.8v14.8c0,2.8,0.9,4.1,3.1,4.2l1.9,0.1v1.1H106.1z"/>
|
||||
<path d="M147.3,80.5c-3,0-4.2-1.4-4.6-5.3c-4.4,3.7-7.3,5.3-10.5,5.3c-4.5,0-7.6-3.2-7.6-7.7c0-2.4,1-4.8,2.6-6.3
|
||||
c3.1-2.7,4.3-3.3,15.4-7.8v-4.4c0-3.9-1.9-6-5.5-6c-2.9,0-5.2,1.6-5.2,3.5c0,0.5,0.1,1.1,0.2,1.7c0.1,0.5,0.1,0.9,0.1,1.2
|
||||
c0,1.6-1.5,3-3.2,3s-3.1-1.4-3.1-3.1c0-1.8,1.2-3.9,3-5.4c2-1.7,5.5-2.7,9.1-2.7c4.4,0,7.5,1.4,9,4.2c1,1.7,1.4,3.7,1.4,7.3v14
|
||||
c0,3.2,0.5,4.2,2.2,4.2c1.1,0,1.9-0.4,3.2-1.4v1.9C151.3,79.6,149.8,80.5,147.3,80.5z M142.6,60.5c-8.7,3.2-11.7,5.8-11.7,10v0.3
|
||||
c0,3.1,2,5.5,4.5,5.5c1.5,0,3.5-0.6,5.3-1.6c1.5-0.9,1.9-1.6,1.9-3.8V60.5z"/>
|
||||
<path d="M165.3,80.5c-4.2,0-6.3-3.1-6.3-9.1V49.7h-3.8c-0.2-0.1-0.3-0.3-0.3-0.5c0-0.4,0.4-0.9,1.2-1.4
|
||||
c1.9-1.1,4.3-3.7,7-7.7c0.5-0.6,1-1.3,1.4-2c0.4,0,0.5,0.2,0.5,0.9v8.4h7.3v2.3h-7.3v20.6c0,4.6,1.1,6.5,3.7,6.5
|
||||
c1.6,0,2.7-0.6,4.3-2.5l0.9,0.8C171.8,78.7,169,80.5,165.3,80.5z"/>
|
||||
<path d="M193.8,79.7v-1.1c4.1-0.4,4.9-1.3,4.9-6.2V58.1c0-5-1.8-7.6-5.4-7.6c-2.8,0-5,1.2-8,4.5v17.4
|
||||
c0,5,0.7,5.8,4.9,6.3v1.1h-15.6v-1.1c4.2-0.6,4.6-1.2,4.6-6.3V38.5c0-3.1-0.6-3.7-3.7-3.7c-0.4,0-0.6,0-0.9,0.1v-1.2l1.9-0.6
|
||||
c4-1.2,5.8-1.7,8.3-2.6l0.4,0.2v21.9c3.3-4.3,6.3-6,10.6-6c5.9,0,8.9,3.9,8.9,11.5v14.3c0,5,0.4,5.5,4.3,6.3v1.1h-15.2V79.7z"/>
|
||||
<path d="M59.1,116.1v-4.3l30.2-14.9v5.3l-23.8,11.7l23.8,11.7v5.3L59.1,116.1z"/>
|
||||
<path d="M135.1,131.9v-1.1c4.2-0.5,4.8-1.1,4.8-5.2v-15.1c0-6-1.3-7.9-5.4-7.9c-3.3,0-5.7,1.3-7.8,4.4v18.1
|
||||
c0,4.5,1.1,5.7,5.2,5.8v1.1h-16.1v-1.1c4.1-0.3,4.9-1.1,4.9-5.1v-15.7c0-5-1.6-7.6-4.8-7.6c-2.5,0-5.6,1.2-7.4,2.9
|
||||
c-0.5,0.5-1.1,1.4-1.1,1.4v20.3c0,2.8,1.1,3.6,4.9,3.7v1.1h-16v-1.1c4-0.1,5-1.2,5-5v-18.2c0-3.5-0.6-4.6-2.5-4.6
|
||||
c-0.8,0-1.4,0.1-2.3,0.3v-1.2c4-1.1,6.4-1.9,10.1-3.2l0.5,0.1v5.4c6-4.5,8-5.5,11.2-5.5c3.9,0,6.3,1.9,7.6,6c3.9-4.2,7.6-6,11.7-6
|
||||
c5.5,0,8.4,4.3,8.4,12.8v14.8c0,2.8,0.9,4.1,3.1,4.2l1.9,0.1v1.1H135.1z"/>
|
||||
<path d="M152.1,131.9v-1.1c5-0.3,5.7-1.1,5.7-6.3v-16.6c0-3.2-0.6-4.3-2.4-4.3c-0.6,0-1.6,0.1-2.4,0.2l-0.6,0.1v-1.1
|
||||
l11.2-4L164,99v25.6c0,5.2,0.6,5.9,5.3,6.3v1.1L152.1,131.9L152.1,131.9z M160.8,93.1c-2,0-3.7-1.6-3.7-3.7c0-2,1.7-3.7,3.7-3.7
|
||||
c2.1,0,3.7,1.7,3.7,3.7C164.6,91.6,163,93.1,160.8,93.1z"/>
|
||||
<path d="M175.8,131v-5.3l23.7-11.8l-23.7-11.7v-5.3l30.1,14.9v4.3L175.8,131z"/>
|
||||
<path d="M31.1,169.5v-4.3l30.2-14.9v5.3l-23.8,11.7L61.3,179v5.3L31.1,169.5z"/>
|
||||
<path d="M71.3,186.4h-4.9l16.5-49.7h4.8L71.3,186.4z"/>
|
||||
<path d="M127.1,185.3v-1.1c4.2-0.5,4.8-1.1,4.8-5.2v-15.2c0-6-1.3-7.9-5.4-7.9c-3.3,0-5.7,1.3-7.8,4.4v18.1
|
||||
c0,4.5,1.1,5.7,5.2,5.8v1.1h-16.1v-1.1c4.1-0.3,4.9-1.1,4.9-5.1v-15.6c0-5-1.6-7.6-4.8-7.6c-2.5,0-5.6,1.2-7.4,2.9
|
||||
c-0.5,0.5-1.1,1.4-1.1,1.4v20.3c0,2.8,1.1,3.6,4.9,3.7v1.1h-16v-1.1c4-0.1,5-1.2,5-5V161c0-3.5-0.6-4.6-2.5-4.6
|
||||
c-0.8,0-1.4,0.1-2.3,0.3v-1.2c4-1.1,6.4-1.9,10.1-3.2l0.5,0.1v5.4c6-4.5,8-5.5,11.2-5.5c3.9,0,6.3,1.9,7.6,6c3.9-4.2,7.6-6,11.7-6
|
||||
c5.5,0,8.4,4.3,8.4,12.8v14.8c0,2.8,0.9,4.1,3.1,4.2l1.9,0.1v1.1H127.1L127.1,185.3z"/>
|
||||
<path d="M168.3,186.1c-3,0-4.2-1.4-4.6-5.3c-4.4,3.7-7.3,5.3-10.5,5.3c-4.5,0-7.6-3.2-7.6-7.7c0-2.4,1-4.8,2.6-6.3
|
||||
c3.1-2.7,4.3-3.3,15.4-7.8v-4.4c0-3.9-1.9-6-5.5-6c-2.9,0-5.2,1.6-5.2,3.5c0,0.5,0.1,1.1,0.2,1.7c0.1,0.5,0.1,0.9,0.1,1.2
|
||||
c0,1.6-1.5,3-3.2,3s-3.1-1.4-3.1-3.1c0-1.8,1.2-3.9,3-5.4c2-1.7,5.5-2.7,9.1-2.7c4.4,0,7.5,1.4,9,4.2c1,1.7,1.4,3.7,1.4,7.3v14
|
||||
c0,3.2,0.5,4.2,2.2,4.2c1.1,0,1.9-0.4,3.2-1.4v1.9C172.3,185.2,170.8,186.1,168.3,186.1z M163.8,166.1c-8.7,3.2-11.7,5.8-11.7,10
|
||||
v0.3c0,3.1,2,5.5,4.5,5.5c1.5,0,3.5-0.6,5.3-1.6c1.5-0.9,1.9-1.6,1.9-3.8V166.1z"/>
|
||||
<path d="M186.3,186.1c-4.2,0-6.3-3.1-6.3-9.1v-21.7h-3.8c-0.2-0.1-0.3-0.3-0.3-0.5c0-0.4,0.4-0.9,1.2-1.4
|
||||
c1.9-1.1,4.3-3.7,7-7.7c0.5-0.6,1-1.3,1.4-2c0.4,0,0.5,0.2,0.5,0.9v8.4h7.3v2.3h-7.3v20.6c0,4.6,1.1,6.5,3.7,6.5
|
||||
c1.6,0,2.7-0.6,4.3-2.5l0.9,0.8C192.8,184.3,190,186.1,186.3,186.1z"/>
|
||||
<path d="M209.1,185.3h-13.4v-1.1c4.2-0.6,4.6-1.2,4.6-6.3V144c0-3.1-0.6-3.7-3.7-3.7c-0.4,0-0.6,0-0.9,0.1v-1.2
|
||||
l1.9-0.6c4-1.2,5.8-1.7,8.3-2.6l0.4,0.2v21.9c0.9-1.2,1.9-2.2,2.8-3.1"/>
|
||||
<path d="M209.1,157.9c-0.8,0.7-1.7,1.5-2.7,2.6v17.4c0,4,0.4,5.3,2.7,5.9"/>
|
||||
</g>
|
||||
<g>
|
||||
<polyline opacity="0.2" fill="#231F20" points="209.1,76.4 118.7,186.5 139.1,186.4 209.1,121 209.1,76.4 "/>
|
||||
<polyline opacity="0.4" fill="#231F20" points="209.1,76.2 118.5,186.5 129.7,186.4 200.2,120.3 209.1,100.8 209.1,76.4 "/>
|
||||
<path fill="#FFD761" d="M121.6,88.7l0.8,87.5l62.3-56.7c0,0-15.3-25.8-24.8-30C151.1,85.6,121.6,88.7,121.6,88.7z"/>
|
||||
<path fill="#FEA01E" d="M209.1,19.5h-54l-33.5,69.2c0,0,29.7-3.4,38.3,0.8c8.9,4.4,25,30.8,25,30.8l24.2-50V19.5z"/>
|
||||
<path d="M120.4,153.7l-0.6,25l23.8-16.9c0,0-8-7-11.2-8.1C129.4,152.8,120.4,153.7,120.4,153.7z"/>
|
||||
<polyline fill="none" stroke="#231F20" stroke-width="5" points="153.9,19.5 121.6,88.7 120.7,181.2 186.6,120.3 209.1,70.3 "/>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
|
||||
<g id="svg_eof"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |