Refactor canvas in multiple pieces to increase maintainability (#446)

* #refactor-canvas getJsonFromSvgElement and svgroot code moved to separate file
* #refactor-canvas build files changes
* #refactor-canvas addSVGElementsFromJson move to json file
* #refactor-canvas selected element option function move separate file
* #refactor-canvas moveUpDownSelected move to select-elem
* ##refactor-canvas build file updated
* #refactor-canvas moveSelectedElements  move to selected-elem
* #refactor-canvas cloneSelectedElements move to slected-elem
* #refactor-canvas alignSelectedElements move to selected-elem
* #refactor-canvas deleteSelectedElements move to selected-elem
* #refactor-canvas copySelectedElements and groupSelectedElements move to selected-elem
* #refactor-canvas pushGroupProperty, ungroupSelectedElement move to selected-elem
* #refactor-canvas comment changes
* #refactor-canvas UndoManager move to separate file
* #refactor-canvas event file move to mouseMove, mouseUpEvent and dblClickEvent
* #refactor-canvas mouseDown move to event
* #refactor-canvas move to undo file
* #refactor alignment changes  and set function revert return
* #refactor-canvas textaction move to separate file
* #refactor-canvas paste-element function move to separate file
* #refactor-canvas set and get  method move to separate file
* #refactor-canvas set and get function moved changes
* #refactor clear file import and regaring function moved changes changes
* #refactor-canvas svg related function move to separate file
* #refactor-canvas setBackground methos move to elem-get-set file
* #refactor-canvas getBsplinePoint method move to event file
* #refactor-canvas export functions move to svg-exec
* #refactor-canvas svg related function moved separate file
* #refactor-canvas updateCanvas,  cycleElement move to selected-elem file
* #refactor-canvas removeUnusedDefElems move to svg-exec file
* #refactor-canvas blur method move to separate file blur-event.js
* #refactor-canvas selection related function move to separate file slection.js
* #refactor-canvas convertGradients, mousewheelmove event bind function move to other files
* #refactor-canvas extension function move to selection file
* #refactor-canvas svg tag long string changes to es6 format
* eslint fixes
* eslint and test fixes
* add netlify logo per their requirements
* #refactor-canvas path file separate to path-method.js and path-actions.js
* #refactor-canvas lint issue fixed
* #refactor-canvas path.js file class and const move to path-method.js
* #refactor-canvas eslint issue fixed. 'jsdoc/check-examples': 'off' already so removed eslint-disable jsdoc/check-examples
* #refactor-canvas path class moved changes and copy-elem.js file cypress test issue fixed
* #refactor-canvas UI - Clipboard paste element  cypress issue fixed
* #refactor-canvas cypress test cases issue fixed changes
* #refactor-canvas cypress test cases issue fixed changes
* #refactor-canvas cypress test case issue fixed
* npm update and fix a few eslint new errors
* fix snapshot and run tests
* add star tool to cypress
* #refactor-canvas shapelibrary, star, polygon and panning tool issue fixed
* build
* Update layer.js
* revert proper declarations

Authored-by OptimistikSAS
This commit is contained in:
JFH
2020-11-11 11:38:45 +01:00
committed by GitHub
parent 087fa44cea
commit 28019eef07
106 changed files with 10353 additions and 7877 deletions

View File

@@ -28,11 +28,7 @@ function transformToString (xform) {
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 + ')';
}
text = m.a === m.d ? 'scale(' + m.a + ')' : 'scale(' + m.a + ',' + m.d + ')';
break;
case 4: { // ROTATE
let cx = 0;

View File

@@ -315,11 +315,7 @@ export const convertToXMLReferences = function (input) {
let output = '';
[...input].forEach((ch) => {
const c = ch.charCodeAt();
if (c <= 127) {
output += ch;
} else {
output += `&#${c};`;
}
output += c <= 127 ? ch : `&#${c};`;
});
return output;
};
@@ -778,18 +774,16 @@ export const getPathDFromElement = function (elem) {
h = b.height;
num = 4 - num; // Why? Because!
if (!rx && !ry) {
// Regular rect
d = getPathDFromSegments([
d = !rx && !ry
? getPathDFromSegments([
['M', [x, y]],
['L', [x + w, y]],
['L', [x + w, y + h]],
['L', [x, y + h]],
['L', [x, y]],
['Z', []]
]);
} else {
d = getPathDFromSegments([
])
: getPathDFromSegments([
['M', [x, y + ry]],
['C', [x, y + ry / num, x + rx / num, y, x + rx, y]],
['L', [x + w - rx, y]],
@@ -801,7 +795,6 @@ export const getPathDFromElement = function (elem) {
['L', [x, y + ry]],
['Z', []]
]);
}
break;
} default:
break;

View File

@@ -58,11 +58,7 @@ export default {
svgCanvas.bind('setnonce', setArrowNonce);
svgCanvas.bind('unsetnonce', unsetArrowNonce);
if (randomizeIds) {
arrowprefix = prefix + nonce + '_';
} else {
arrowprefix = prefix;
}
arrowprefix = randomizeIds ? prefix + nonce + '_' : prefix;
const pathdata = {
fw: {d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw'},

View File

@@ -189,10 +189,7 @@ export default {
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'; }
const seType = val.substr(0, 1) === '\\' ? val.substr(1) : 'textmarker';
if (!markerTypes[seType]) { return undefined; } // an unknown type!

View File

@@ -2577,11 +2577,7 @@ editor.init = () => {
$(context).parentsUntil('#svgcontent > g').andSelf().each(function () {
if (this.id) {
str += ' > ' + this.id;
if (this !== context) {
linkStr += ' > <a href="#">' + this.id + '</a>';
} else {
linkStr += ' > ' + this.id;
}
linkStr += this !== context ? ' > <a href="#">' + this.id + '</a>' : ' > ' + this.id;
}
});
@@ -2684,12 +2680,7 @@ editor.init = () => {
if (toolButtonClick(showSel)) {
options.fn();
}
let icon;
if (options.icon) {
icon = $.getSvgIcon(options.icon, true);
} else {
icon = $(options.sel).children().eq(0).clone();
}
const icon = options.icon ? $.getSvgIcon(options.icon, true) : $(options.sel).children().eq(0).clone();
icon[0].setAttribute('width', shower.width());
icon[0].setAttribute('height', shower.height());
@@ -3280,11 +3271,7 @@ editor.init = () => {
const opts = {alpha: opac};
if (color.startsWith('url(#')) {
let refElem = svgCanvas.getRefElem(color);
if (refElem) {
refElem = refElem.cloneNode(true);
} else {
refElem = $('#' + type + '_color defs *')[0];
}
refElem = refElem ? refElem.cloneNode(true) : $('#' + type + '_color defs *')[0];
opts[refElem.tagName] = refElem;
} else if (color.startsWith('#')) {
opts.solidColor = color.substr(1);
@@ -3348,14 +3335,12 @@ editor.init = () => {
const colorBlocks = ['#FFF', '#888', '#000', 'chessboard'];
str = '';
$.each(colorBlocks, function (i, e) {
if (e === 'chessboard') {
str += '<div class="color_block" data-bgcolor="' + e +
str += e === 'chessboard'
? '<div class="color_block" data-bgcolor="' + e +
'" style="background-image:url(data:image/gif;base64,' +
'R0lGODlhEAAQAIAAAP///9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG+' +
'gq4jM3IFLJgpswNly/XkcBpIiVaInlLJr9FZWAQA7);"></div>';
} else {
str += '<div class="color_block" data-bgcolor="' + e + '" style="background-color:' + e + ';"></div>';
}
'gq4jM3IFLJgpswNly/XkcBpIiVaInlLJr9FZWAQA7);"></div>'
: '<div class="color_block" data-bgcolor="' + e + '" style="background-color:' + e + ';"></div>';
});
$('#bg_blocks').append(str);
const blocks = $('#bg_blocks div');
@@ -5627,12 +5612,7 @@ editor.init = () => {
const menu = ($(sel).parents('#main_menu').length);
$(sel).each(function () {
let t;
if (menu) {
t = $(this).text().split(' [')[0];
} else {
t = this.title.split(' [')[0];
}
const t = menu ? $(this).text().split(' [')[0] : this.title.split(' [')[0];
let keyStr = '';
// Shift+Up
$.each(keyval.split('/'), function (i, key) {

161
src/svgcanvas/blur-event.js Normal file
View File

@@ -0,0 +1,161 @@
/**
* Tools for blur event.
* @module blur
* @license MIT
* @copyright 2011 Jeff Schiller
*/
import * as hstry from './history.js';
const {
InsertElementCommand, ChangeElementCommand, BatchCommand
} = hstry;
let blurContext_ = null;
/**
* @function module:blur.init
* @param {module:blur.blurContext} blurContext
* @returns {void}
*/
export const init = function (blurContext) {
blurContext_ = blurContext;
};
/**
* Sets the `stdDeviation` blur value on the selected element without being undoable.
* @function module:svgcanvas.SvgCanvas#setBlurNoUndo
* @param {Float} val - The new `stdDeviation` value
* @returns {void}
*/
export const setBlurNoUndo = function (val) {
const selectedElements = blurContext_.getSelectedElements();
if (!blurContext_.getFilter()) {
blurContext_.getCanvas().setBlur(val);
return;
}
if (val === 0) {
// Don't change the StdDev, as that will hide the element.
// Instead, just remove the value for "filter"
blurContext_.changeSelectedAttributeNoUndoMethod('filter', '');
blurContext_.setFilterHidden(true);
} else {
const elem = selectedElements[0];
if (blurContext_.getFilterHidden()) {
blurContext_.changeSelectedAttributeNoUndoMethod('filter', 'url(#' + elem.id + '_blur)');
}
if (blurContext_.isWebkit()) {
// console.log('e', elem); // eslint-disable-line no-console
elem.removeAttribute('filter');
elem.setAttribute('filter', 'url(#' + elem.id + '_blur)');
}
const filter = blurContext_.getFilter();
blurContext_.changeSelectedAttributeNoUndoMethod('stdDeviation', val, [filter.firstChild]);
blurContext_.getCanvas().setBlurOffsets(filter, val);
}
};
/**
*
* @returns {void}
*/
function finishChange () {
const bCmd = blurContext_.getCanvas().undoMgr.finishUndoableChange();
blurContext_.getCurCommand().addSubCommand(bCmd);
blurContext_.addCommandToHistory(blurContext_.getCurCommand());
blurContext_.setCurCommand(null);
blurContext_.setFilter(null);
}
/**
* Sets the `x`, `y`, `width`, `height` values of the filter element in order to
* make the blur not be clipped. Removes them if not neeeded.
* @function module:svgcanvas.SvgCanvas#setBlurOffsets
* @param {Element} filterElem - The filter DOM element to update
* @param {Float} stdDev - The standard deviation value on which to base the offset size
* @returns {void}
*/
export const setBlurOffsets = function (filterElem, stdDev) {
if (stdDev > 3) {
// TODO: Create algorithm here where size is based on expected blur
blurContext_.getCanvas().assignAttributes(filterElem, {
x: '-50%',
y: '-50%',
width: '200%',
height: '200%'
}, 100);
// Removing these attributes hides text in Chrome (see Issue 579)
} else if (!blurContext_.isWebkit()) {
filterElem.removeAttribute('x');
filterElem.removeAttribute('y');
filterElem.removeAttribute('width');
filterElem.removeAttribute('height');
}
};
/**
* Adds/updates the blur filter to the selected element.
* @function module:svgcanvas.SvgCanvas#setBlur
* @param {Float} val - Float with the new `stdDeviation` blur value
* @param {boolean} complete - Whether or not the action should be completed (to add to the undo manager)
* @returns {void}
*/
export const setBlur = function (val, complete) {
const selectedElements = blurContext_.getSelectedElements();
if (blurContext_.getCurCommand()) {
finishChange();
return;
}
// Looks for associated blur, creates one if not found
const elem = selectedElements[0];
const elemId = elem.id;
blurContext_.setFilter(blurContext_.getCanvas().getElem(elemId + '_blur'));
val -= 0;
const batchCmd = new BatchCommand();
// Blur found!
if (blurContext_.getFilter()) {
if (val === 0) {
blurContext_.setFilter(null);
}
} else {
// Not found, so create
const newblur = blurContext_.getCanvas().addSVGElementFromJson({element: 'feGaussianBlur',
attr: {
in: 'SourceGraphic',
stdDeviation: val
}
});
blurContext_.setFilter(blurContext_.getCanvas().addSVGElementFromJson({element: 'filter',
attr: {
id: elemId + '_blur'
}
}));
blurContext_.getFilter().append(newblur);
blurContext_.getCanvas().findDefs().append(blurContext_.getFilter());
batchCmd.addSubCommand(new InsertElementCommand(blurContext_.getFilter()));
}
const changes = {filter: elem.getAttribute('filter')};
if (val === 0) {
elem.removeAttribute('filter');
batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
return;
}
blurContext_.changeSelectedAttributeMethod('filter', 'url(#' + elemId + '_blur)');
batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
blurContext_.getCanvas().setBlurOffsets(blurContext_.getFilter(), val);
const filter = blurContext_.getFilter();
blurContext_.setCurCommand(batchCmd);
blurContext_.getCanvas().undoMgr.beginUndoableChange('stdDeviation', [filter ? filter.firstChild : null]);
if (complete) {
blurContext_.getCanvas().setBlurNoUndo(val);
finishChange();
}
};

45
src/svgcanvas/clear.js Normal file
View File

@@ -0,0 +1,45 @@
/* globals jQuery */
/**
* Tools for clear.
* @module clear
* @license MIT
* @copyright 2011 Jeff Schiller
*/
import jQueryPluginSVG from '../common/jQuery.attr.js';
import {NS} from '../common/namespaces.js';
const $ = jQueryPluginSVG(jQuery);
let clearContext_ = null;
/**
* @function module:clear.init
* @param {module:clear.SvgCanvas#init} clearContext
* @returns {void}
*/
export const init = function (clearContext) {
clearContext_ = clearContext;
};
export const clearSvgContentElementInit = function () {
const curConfig = clearContext_.getCurConfig();
const {dimensions} = curConfig;
$(clearContext_.getSVGContent()).empty();
// TODO: Clear out all other attributes first?
$(clearContext_.getSVGContent()).attr({
id: 'svgcontent',
width: dimensions[0],
height: dimensions[1],
x: dimensions[0],
y: dimensions[1],
overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden',
xmlns: NS.SVG,
'xmlns:se': NS.SE,
'xmlns:xlink': NS.XLINK
}).appendTo(clearContext_.getSVGRoot());
// TODO: make this string optional and set by the client
const comment = clearContext_.getDOMDocument().createComment(' Created with SVG-edit - https://github.com/SVG-Edit/svgedit');
clearContext_.getSVGContent().append(comment);
};

View File

@@ -0,0 +1,886 @@
/* globals jQuery */
/**
* @module elem-get-set get and set methods.
* @license MIT
* @copyright 2011 Jeff Schiller
*/
import * as hstry from './history.js';
import jQueryPluginSVG from '../common/jQuery.attr.js';
import {NS} from '../common/namespaces.js';
import {
getVisibleElements, getStrokedBBoxDefaultVisible, findDefs,
walkTree, isNullish, getHref, setHref, getElem
} from '../common/utilities.js';
import {
convertToNum
} from '../common/units.js';
const $ = jQueryPluginSVG(jQuery);
const {
InsertElementCommand, RemoveElementCommand,
ChangeElementCommand, BatchCommand
} = hstry;
let elemContext_ = null;
/**
* @function module:elem-get-set.init
* @param {module:elem-get-set.elemContext} elemContext
* @returns {void}
*/
export const init = function (elemContext) {
elemContext_ = elemContext;
};
/**
* @function module:elem-get-set.SvgCanvas#getResolution
* @returns {DimensionsAndZoom} The current dimensions and zoom level in an object
*/
export const getResolutionMethod = function () {
const currentZoom = elemContext_.getCurrentZoom();
const w = elemContext_.getSVGContent().getAttribute('width') / currentZoom;
const h = elemContext_.getSVGContent().getAttribute('height') / currentZoom;
return {
w,
h,
zoom: currentZoom
};
};
/**
* @function module:elem-get-set.SvgCanvas#getTitle
* @param {Element} [elem]
* @returns {string|void} the current group/SVG's title contents or
* `undefined` if no element is passed nd there are no selected elements.
*/
export const getTitleMethod = function (elem) {
const selectedElements = elemContext_.getSelectedElements();
elem = elem || selectedElements[0];
if (!elem) { return undefined; }
elem = $(elem).data('gsvg') || $(elem).data('symbol') || elem;
const childs = elem.childNodes;
for (const child of childs) {
if (child.nodeName === 'title') {
return child.textContent;
}
}
return '';
};
/**
* Sets the group/SVG's title content.
* @function module:elem-get-set.SvgCanvas#setGroupTitle
* @param {string} val
* @todo Combine this with `setDocumentTitle`
* @returns {void}
*/
export const setGroupTitleMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
let elem = selectedElements[0];
elem = $(elem).data('gsvg') || elem;
const ts = $(elem).children('title');
const batchCmd = new BatchCommand('Set Label');
let title;
if (!val.length) {
// Remove title element
const tsNextSibling = ts.nextSibling;
batchCmd.addSubCommand(new RemoveElementCommand(ts[0], tsNextSibling, elem));
ts.remove();
} else if (ts.length) {
// Change title contents
title = ts[0];
batchCmd.addSubCommand(new ChangeElementCommand(title, {'#text': title.textContent}));
title.textContent = val;
} else {
// Add title element
title = elemContext_.getDOMDocument().createElementNS(NS.SVG, 'title');
title.textContent = val;
$(elem).prepend(title);
batchCmd.addSubCommand(new InsertElementCommand(title));
}
elemContext_.addCommandToHistory(batchCmd);
};
/**
* Adds/updates a title element for the document with the given name.
* This is an undoable action.
* @function module:elem-get-set.SvgCanvas#setDocumentTitle
* @param {string} newTitle - String with the new title
* @returns {void}
*/
export const setDocumentTitleMethod = function (newTitle) {
const childs = elemContext_.getSVGContent().childNodes;
let docTitle = false, oldTitle = '';
const batchCmd = new BatchCommand('Change Image Title');
for (const child of childs) {
if (child.nodeName === 'title') {
docTitle = child;
oldTitle = docTitle.textContent;
break;
}
}
if (!docTitle) {
docTitle = elemContext_.getDOMDocument().createElementNS(NS.SVG, 'title');
elemContext_.getSVGContent().insertBefore(docTitle, elemContext_.getSVGContent().firstChild);
// svgcontent.firstChild.before(docTitle); // Ok to replace above with this?
}
if (newTitle.length) {
docTitle.textContent = newTitle;
} else {
// No title given, so element is not necessary
docTitle.remove();
}
batchCmd.addSubCommand(new ChangeElementCommand(docTitle, {'#text': oldTitle}));
elemContext_.addCommandToHistory(batchCmd);
};
/**
* Changes the document's dimensions to the given size.
* @function module:elem-get-set.SvgCanvas#setResolution
* @param {Float|"fit"} x - Number with the width of the new dimensions in user units.
* Can also be the string "fit" to indicate "fit to content".
* @param {Float} y - Number with the height of the new dimensions in user units.
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {boolean} Indicates if resolution change was successful.
* It will fail on "fit to content" option with no content to fit to.
*/
export const setResolutionMethod = function (x, y) {
const currentZoom = elemContext_.getCurrentZoom();
const res = elemContext_.getCanvas().getResolution();
const {w, h} = res;
let batchCmd;
if (x === 'fit') {
// Get bounding box
const bbox = getStrokedBBoxDefaultVisible();
if (bbox) {
batchCmd = new BatchCommand('Fit Canvas to Content');
const visEls = getVisibleElements();
elemContext_.getCanvas().addToSelection(visEls);
const dx = [], dy = [];
$.each(visEls, function (i, item) {
dx.push(bbox.x * -1);
dy.push(bbox.y * -1);
});
const cmd = elemContext_.getCanvas().moveSelectedElements(dx, dy, true);
batchCmd.addSubCommand(cmd);
elemContext_.getCanvas().clearSelection();
x = Math.round(bbox.width);
y = Math.round(bbox.height);
} else {
return false;
}
}
if (x !== w || y !== h) {
if (!batchCmd) {
batchCmd = new BatchCommand('Change Image Dimensions');
}
x = convertToNum('width', x);
y = convertToNum('height', y);
elemContext_.getSVGContent().setAttribute('width', x);
elemContext_.getSVGContent().setAttribute('height', y);
this.contentW = x;
this.contentH = y;
batchCmd.addSubCommand(new ChangeElementCommand(elemContext_.getSVGContent(), {width: w, height: h}));
elemContext_.getSVGContent().setAttribute('viewBox', [0, 0, x / currentZoom, y / currentZoom].join(' '));
batchCmd.addSubCommand(new ChangeElementCommand(elemContext_.getSVGContent(), {viewBox: ['0 0', w, h].join(' ')}));
elemContext_.addCommandToHistory(batchCmd);
elemContext_.call('changed', [elemContext_.getSVGContent()]);
}
return true;
};
/**
* Returns the editor's namespace URL, optionally adding it to the root element.
* @function module:elem-get-set.SvgCanvas#getEditorNS
* @param {boolean} [add] - Indicates whether or not to add the namespace value
* @returns {string} The editor's namespace URL
*/
export const getEditorNSMethod = function (add) {
if (add) {
elemContext_.getSVGContent().setAttribute('xmlns:se', NS.SE);
}
return NS.SE;
};
/**
* @typedef {PlainObject} module:elem-get-set.ZoomAndBBox
* @property {Float} zoom
* @property {module:utilities.BBoxObject} bbox
*/
/**
* Sets the zoom level on the canvas-side based on the given value.
* @function module:elem-get-set.SvgCanvas#setBBoxZoom
* @param {"selection"|"canvas"|"content"|"layer"|module:SVGEditor.BBoxObjectWithFactor} val - Bounding box object to zoom to or string indicating zoom option. Note: the object value type is defined in `svg-editor.js`
* @param {Integer} editorW - The editor's workarea box's width
* @param {Integer} editorH - The editor's workarea box's height
* @returns {module:elem-get-set.ZoomAndBBox|void}
*/
export const setBBoxZoomMethod = function (val, editorW, editorH) {
const currentZoom = elemContext_.getCurrentZoom();
const selectedElements = elemContext_.getSelectedElements();
let spacer = 0.85;
let bb;
const calcZoom = function (bb) { // eslint-disable-line no-shadow
if (!bb) { return false; }
const wZoom = Math.round((editorW / bb.width) * 100 * spacer) / 100;
const hZoom = Math.round((editorH / bb.height) * 100 * spacer) / 100;
const zoom = Math.min(wZoom, hZoom);
elemContext_.getCanvas().setZoom(zoom);
return {zoom, bbox: bb};
};
if (typeof val === 'object') {
bb = val;
if (bb.width === 0 || bb.height === 0) {
const newzoom = bb.zoom ? bb.zoom : currentZoom * bb.factor;
elemContext_.getCanvas().setZoom(newzoom);
return {zoom: currentZoom, bbox: bb};
}
return calcZoom(bb);
}
switch (val) {
case 'selection': {
if (!selectedElements[0]) { return undefined; }
const selectedElems = $.map(selectedElements, function (n) {
if (n) {
return n;
}
return undefined;
});
bb = getStrokedBBoxDefaultVisible(selectedElems);
break;
} case 'canvas': {
const res = elemContext_.getCanvas().getResolution();
spacer = 0.95;
bb = {width: res.w, height: res.h, x: 0, y: 0};
break;
} case 'content':
bb = getStrokedBBoxDefaultVisible();
break;
case 'layer':
bb = getStrokedBBoxDefaultVisible(getVisibleElements(elemContext_.getCanvas().getCurrentDrawing().getCurrentLayer()));
break;
default:
return undefined;
}
return calcZoom(bb);
};
/**
* Sets the zoom to the given level.
* @function module:elem-get-set.SvgCanvas#setZoom
* @param {Float} zoomLevel - Float indicating the zoom level to change to
* @fires module:elem-get-set.SvgCanvas#event:ext_zoomChanged
* @returns {void}
*/
export const setZoomMethod = function (zoomLevel) {
const selectedElements = elemContext_.getSelectedElements();
const res = elemContext_.getCanvas().getResolution();
elemContext_.getSVGContent().setAttribute('viewBox', '0 0 ' + res.w / zoomLevel + ' ' + res.h / zoomLevel);
elemContext_.setCurrentZoom(zoomLevel);
$.each(selectedElements, function (i, elem) {
if (!elem) { return; }
elemContext_.getCanvas().selectorManager.requestSelector(elem).resize();
});
elemContext_.getCanvas().pathActions.zoomChange();
elemContext_.getCanvas().runExtensions('zoomChanged', zoomLevel);
};
/**
* Change the current stroke/fill color/gradient value.
* @function module:elem-get-set.SvgCanvas#setColor
* @param {string} type - String indicating fill or stroke
* @param {string} val - The value to set the stroke attribute to
* @param {boolean} preventUndo - Boolean indicating whether or not this should be an undoable option
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
export const setColorMethod = function (type, val, preventUndo) {
const selectedElements = elemContext_.getSelectedElements();
elemContext_.setCurShape(type, val);
elemContext_.setCurProperties(type + '_paint', {type: 'solidColor'});
const elems = [];
/**
*
* @param {Element} e
* @returns {void}
*/
function addNonG (e) {
if (e.nodeName !== 'g') {
elems.push(e);
}
}
let i = selectedElements.length;
while (i--) {
const elem = selectedElements[i];
if (elem) {
if (elem.tagName === 'g') {
walkTree(elem, addNonG);
} else if (type === 'fill') {
if (elem.tagName !== 'polyline' && elem.tagName !== 'line') {
elems.push(elem);
}
} else {
elems.push(elem);
}
}
}
if (elems.length > 0) {
if (!preventUndo) {
elemContext_.getCanvas().changeSelectedAttribute(type, val, elems);
elemContext_.call('changed', elems);
} else {
elemContext_.changeSelectedAttributeNoUndoMethod(type, val, elems);
}
}
};
/**
* Apply the current gradient to selected element's fill or stroke.
* @function module:elem-get-set.SvgCanvas#setGradient
* @param {"fill"|"stroke"} type - String indicating "fill" or "stroke" to apply to an element
* @returns {void}
*/
export const setGradientMethod = function (type) {
if (!elemContext_.getCurProperties(type + '_paint') ||
elemContext_.getCurProperties(type + '_paint').type === 'solidColor') { return; }
const canvas = elemContext_.getCanvas();
let grad = canvas[type + 'Grad'];
// find out if there is a duplicate gradient already in the defs
const duplicateGrad = findDuplicateGradient(grad);
const defs = findDefs();
// no duplicate found, so import gradient into defs
if (!duplicateGrad) {
// const origGrad = grad;
grad = defs.appendChild(elemContext_.getDOMDocument().importNode(grad, true));
// get next id and set it on the grad
grad.id = elemContext_.getCanvas().getNextId();
} else { // use existing gradient
grad = duplicateGrad;
}
elemContext_.getCanvas().setColor(type, 'url(#' + grad.id + ')');
};
/**
* Check if exact gradient already exists.
* @function module:svgcanvas~findDuplicateGradient
* @param {SVGGradientElement} grad - The gradient DOM element to compare to others
* @returns {SVGGradientElement} The existing gradient if found, `null` if not
*/
export const findDuplicateGradient = function (grad) {
const defs = findDefs();
const existingGrads = $(defs).find('linearGradient, radialGradient');
let i = existingGrads.length;
const radAttrs = ['r', 'cx', 'cy', 'fx', 'fy'];
while (i--) {
const og = existingGrads[i];
if (grad.tagName === 'linearGradient') {
if (grad.getAttribute('x1') !== og.getAttribute('x1') ||
grad.getAttribute('y1') !== og.getAttribute('y1') ||
grad.getAttribute('x2') !== og.getAttribute('x2') ||
grad.getAttribute('y2') !== og.getAttribute('y2')
) {
continue;
}
} else {
const gradAttrs = $(grad).attr(radAttrs);
const ogAttrs = $(og).attr(radAttrs);
let diff = false;
$.each(radAttrs, function (j, attr) {
if (gradAttrs[attr] !== ogAttrs[attr]) { diff = true; }
});
if (diff) { continue; }
}
// else could be a duplicate, iterate through stops
const stops = grad.getElementsByTagNameNS(NS.SVG, 'stop');
const ostops = og.getElementsByTagNameNS(NS.SVG, 'stop');
if (stops.length !== ostops.length) {
continue;
}
let j = stops.length;
while (j--) {
const stop = stops[j];
const ostop = ostops[j];
if (stop.getAttribute('offset') !== ostop.getAttribute('offset') ||
stop.getAttribute('stop-opacity') !== ostop.getAttribute('stop-opacity') ||
stop.getAttribute('stop-color') !== ostop.getAttribute('stop-color')) {
break;
}
}
if (j === -1) {
return og;
}
} // for each gradient in defs
return null;
};
/**
* Set a color/gradient to a fill/stroke.
* @function module:elem-get-set.SvgCanvas#setPaint
* @param {"fill"|"stroke"} type - String with "fill" or "stroke"
* @param {module:jGraduate.jGraduatePaintOptions} paint - The jGraduate paint object to apply
* @returns {void}
*/
export const setPaintMethod = function (type, paint) {
// make a copy
const p = new $.jGraduate.Paint(paint);
this.setPaintOpacity(type, p.alpha / 100, true);
// now set the current paint object
elemContext_.setCurProperties(type + '_paint', p);
switch (p.type) {
case 'solidColor':
this.setColor(type, p.solidColor !== 'none' ? '#' + p.solidColor : 'none');
break;
case 'linearGradient':
case 'radialGradient':
elemContext_.setCanvas(type + 'Grad', p[p.type]);
elemContext_.getCanvas().setGradient(type);
break;
}
};
/**
* Sets the stroke width for the current selected elements.
* When attempting to set a line's width to 0, this changes it to 1 instead.
* @function module:elem-get-set.SvgCanvas#setStrokeWidth
* @param {Float} val - A Float indicating the new stroke width value
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
export const setStrokeWidthMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
if (val === 0 && ['line', 'path'].includes(elemContext_.getCanvas().getMode())) {
elemContext_.getCanvas().setStrokeWidth(1);
return;
}
elemContext_.setCurProperties('stroke_width', val);
const elems = [];
/**
*
* @param {Element} e
* @returns {void}
*/
function addNonG (e) {
if (e.nodeName !== 'g') {
elems.push(e);
}
}
let i = selectedElements.length;
while (i--) {
const elem = selectedElements[i];
if (elem) {
if (elem.tagName === 'g') {
walkTree(elem, addNonG);
} else {
elems.push(elem);
}
}
}
if (elems.length > 0) {
elemContext_.getCanvas().changeSelectedAttribute('stroke-width', val, elems);
elemContext_.call('changed', selectedElements);
}
};
/**
* Set the given stroke-related attribute the given value for selected elements.
* @function module:elem-get-set.SvgCanvas#setStrokeAttr
* @param {string} attr - String with the attribute name
* @param {string|Float} val - String or number with the attribute value
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
export const setStrokeAttrMethod = function (attr, val) {
const selectedElements = elemContext_.getSelectedElements();
elemContext_.setCurShape(attr.replace('-', '_'), val);
const elems = [];
let i = selectedElements.length;
while (i--) {
const elem = selectedElements[i];
if (elem) {
if (elem.tagName === 'g') {
walkTree(elem, function (e) { if (e.nodeName !== 'g') { elems.push(e); } });
} else {
elems.push(elem);
}
}
}
if (elems.length > 0) {
elemContext_.getCanvas().changeSelectedAttribute(attr, val, elems);
elemContext_.call('changed', selectedElements);
}
};
/**
* Check whether selected element is bold or not.
* @function module:svgcanvas.SvgCanvas#getBold
* @returns {boolean} Indicates whether or not element is bold
*/
export const getBoldMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
// should only have one element selected
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
return (selected.getAttribute('font-weight') === 'bold');
}
return false;
};
/**
* Make the selected element bold or normal.
* @function module:svgcanvas.SvgCanvas#setBold
* @param {boolean} b - Indicates bold (`true`) or normal (`false`)
* @returns {void}
*/
export const setBoldMethod = function (b) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('font-weight', b ? 'bold' : 'normal');
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* Check whether selected element is in italics or not.
* @function module:svgcanvas.SvgCanvas#getItalic
* @returns {boolean} Indicates whether or not element is italic
*/
export const getItalicMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
return (selected.getAttribute('font-style') === 'italic');
}
return false;
};
/**
* Make the selected element italic or normal.
* @function module:svgcanvas.SvgCanvas#setItalic
* @param {boolean} i - Indicates italic (`true`) or normal (`false`)
* @returns {void}
*/
export const setItalicMethod = function (i) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('font-style', i ? 'italic' : 'normal');
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* @function module:svgcanvas.SvgCanvas#getFontFamily
* @returns {string} The current font family
*/
export const getFontFamilyMethod = function () {
return elemContext_.getCurText('font_family');
};
/**
* Set the new font family.
* @function module:svgcanvas.SvgCanvas#setFontFamily
* @param {string} val - String with the new font family
* @returns {void}
*/
export const setFontFamilyMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
elemContext_.setCurText('font_family', val);
elemContext_.getCanvas().changeSelectedAttribute('font-family', val);
if (selectedElements[0] && !selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* Set the new font color.
* @function module:svgcanvas.SvgCanvas#setFontColor
* @param {string} val - String with the new font color
* @returns {void}
*/
export const setFontColorMethod = function (val) {
elemContext_.setCurText('fill', val);
elemContext_.getCanvas().changeSelectedAttribute('fill', val);
};
/**
* @function module:svgcanvas.SvgCanvas#getFontColor
* @returns {string} The current font color
*/
export const getFontColorMethod = function () {
return elemContext_.getCurText('fill');
};
/**
* @function module:svgcanvas.SvgCanvas#getFontSize
* @returns {Float} The current font size
*/
export const getFontSizeMethod = function () {
return elemContext_.getCurText('font_size');
};
/**
* Applies the given font size to the selected element.
* @function module:svgcanvas.SvgCanvas#setFontSize
* @param {Float} val - Float with the new font size
* @returns {void}
*/
export const setFontSizeMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
elemContext_.setCurText('font_size', val);
elemContext_.getCanvas().changeSelectedAttribute('font-size', val);
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* @function module:svgcanvas.SvgCanvas#getText
* @returns {string} The current text (`textContent`) of the selected element
*/
export const getTextMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (isNullish(selected)) { return ''; }
return selected.textContent;
};
/**
* Updates the text element with the given string.
* @function module:svgcanvas.SvgCanvas#setTextContent
* @param {string} val - String with the new text
* @returns {void}
*/
export const setTextContentMethod = function (val) {
elemContext_.getCanvas().changeSelectedAttribute('#text', val);
elemContext_.getCanvas().textActions.init(val);
elemContext_.getCanvas().textActions.setCursor();
};
/**
* Sets the new image URL for the selected image element. Updates its size if
* a new URL is given.
* @function module:svgcanvas.SvgCanvas#setImageURL
* @param {string} val - String with the image URL/path
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
export const setImageURLMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
const elem = selectedElements[0];
if (!elem) { return; }
const attrs = $(elem).attr(['width', 'height']);
const setsize = (!attrs.width || !attrs.height);
const curHref = getHref(elem);
// Do nothing if no URL change or size change
if (curHref === val && !setsize) {
return;
}
const batchCmd = new BatchCommand('Change Image URL');
setHref(elem, val);
batchCmd.addSubCommand(new ChangeElementCommand(elem, {
'#href': curHref
}));
$(new Image()).load(function () {
const changes = $(elem).attr(['width', 'height']);
$(elem).attr({
width: this.width,
height: this.height
});
elemContext_.getCanvas().selectorManager.requestSelector(elem).resize();
batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
elemContext_.addCommandToHistory(batchCmd);
elemContext_.call('changed', [elem]);
}).attr('src', val);
};
/**
* Sets the new link URL for the selected anchor element.
* @function module:svgcanvas.SvgCanvas#setLinkURL
* @param {string} val - String with the link URL/path
* @returns {void}
*/
export const setLinkURLMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
let elem = selectedElements[0];
if (!elem) { return; }
if (elem.tagName !== 'a') {
// See if parent is an anchor
const parentsA = $(elem).parents('a');
if (parentsA.length) {
elem = parentsA[0];
} else {
return;
}
}
const curHref = getHref(elem);
if (curHref === val) { return; }
const batchCmd = new BatchCommand('Change Link URL');
setHref(elem, val);
batchCmd.addSubCommand(new ChangeElementCommand(elem, {
'#href': curHref
}));
elemContext_.addCommandToHistory(batchCmd);
};
/**
* Sets the `rx` and `ry` values to the selected `rect` element
* to change its corner radius.
* @function module:svgcanvas.SvgCanvas#setRectRadius
* @param {string|Float} val - The new radius
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
export const setRectRadiusMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'rect') {
const r = selected.getAttribute('rx');
if (r !== String(val)) {
selected.setAttribute('rx', val);
selected.setAttribute('ry', val);
elemContext_.addCommandToHistory(new ChangeElementCommand(selected, {rx: r, ry: r}, 'Radius'));
elemContext_.call('changed', [selected]);
}
}
};
/**
* Wraps the selected element(s) in an anchor element or converts group to one.
* @function module:svgcanvas.SvgCanvas#makeHyperlink
* @param {string} url
* @returns {void}
*/
export const makeHyperlinkMethod = function (url) {
elemContext_.getCanvas().groupSelectedElements('a', url);
// TODO: If element is a single "g", convert to "a"
// if (selectedElements.length > 1 && selectedElements[1]) {
};
/**
* @function module:svgcanvas.SvgCanvas#removeHyperlink
* @returns {void}
*/
export const removeHyperlinkMethod = function () {
elemContext_.getCanvas().ungroupSelectedElement();
};
/**
* Group: Element manipulation.
*/
/**
* Sets the new segment type to the selected segment(s).
* @function module:svgcanvas.SvgCanvas#setSegType
* @param {Integer} newType - New segment type. See {@link https://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg} for list
* @returns {void}
*/
export const setSegTypeMethod = function (newType) {
elemContext_.getCanvas().pathActions.setSegType(newType);
};
/**
* Set the background of the editor (NOT the actual document).
* @function module:svgcanvas.SvgCanvas#setBackground
* @param {string} color - String with fill color to apply
* @param {string} url - URL or path to image to use
* @returns {void}
*/
export const setBackgroundMethod = function (color, url) {
const bg = getElem('canvasBackground');
const border = $(bg).find('rect')[0];
let bgImg = getElem('background_image');
let bgPattern = getElem('background_pattern');
border.setAttribute('fill', color === 'chessboard' ? '#fff' : color);
if (color === 'chessboard') {
if (!bgPattern) {
bgPattern = elemContext_.getDOMDocument().createElementNS(NS.SVG, 'foreignObject');
elemContext_.getCanvas().assignAttributes(bgPattern, {
id: 'background_pattern',
width: '100%',
height: '100%',
preserveAspectRatio: 'xMinYMin',
style: 'pointer-events:none'
});
const div = document.createElement('div');
elemContext_.getCanvas().assignAttributes(div, {
style: 'pointer-events:none;width:100%;height:100%;' +
'background-image:url(data:image/gif;base64,' +
'R0lGODlhEAAQAIAAAP///9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG+' +
'gq4jM3IFLJgpswNly/XkcBpIiVaInlLJr9FZWAQA7);'
});
bgPattern.appendChild(div);
bg.append(bgPattern);
}
} else if (bgPattern) {
bgPattern.remove();
}
if (url) {
if (!bgImg) {
bgImg = elemContext_.getDOMDocument().createElementNS(NS.SVG, 'image');
elemContext_.getCanvas().assignAttributes(bgImg, {
id: 'background_image',
width: '100%',
height: '100%',
preserveAspectRatio: 'xMinYMin',
style: 'pointer-events:none'
});
}
setHref(bgImg, url);
bg.append(bgImg);
} else if (bgImg) {
bgImg.remove();
}
};

1388
src/svgcanvas/event.js Normal file

File diff suppressed because it is too large Load Diff

110
src/svgcanvas/json.js Normal file
View File

@@ -0,0 +1,110 @@
/**
* Tools for SVG handle on JSON format.
* @module svgcanvas
* @license MIT
*
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import {getElem, assignAttributes, cleanupElement} from '../common/utilities.js';
import {NS} from '../common/namespaces.js';
let jsonContext_ = null;
let svgdoc_ = null;
/**
* @function module:json.jsonContext#getSelectedElements
* @returns {Element[]} the array with selected DOM elements
*/
/**
* @function module:json.jsonContext#getDOMDocument
* @returns {HTMLDocument}
*/
/**
* @function module:json.init
* @param {module:json.jsonContext} jsonContext
* @returns {void}
*/
export const init = function (jsonContext) {
jsonContext_ = jsonContext;
svgdoc_ = jsonContext.getDOMDocument();
};
/**
* @function module:json.getJsonFromSvgElements Iterate element and return json format
* @param {ArgumentsArray} data - element
* @returns {svgRootElement}
*/
export const getJsonFromSvgElements = (data) => {
// Text node
if (data.nodeType === 3) return data.nodeValue;
const retval = {
element: data.tagName,
// namespace: nsMap[data.namespaceURI],
attr: {},
children: []
};
// Iterate attributes
for (let i = 0, attr; (attr = data.attributes[i]); i++) {
retval.attr[attr.name] = attr.value;
}
// Iterate children
for (let i = 0, node; (node = data.childNodes[i]); i++) {
retval.children[i] = getJsonFromSvgElements(node);
}
return retval;
};
/**
* This should really be an intersection implementing all rather than a union.
* @name module:json.addSVGElementsFromJson
* @type {module:utilities.EditorContext#addSVGElementsFromJson|module:path.EditorContext#addSVGElementFromJson}
*/
export const addSVGElementsFromJson = function (data) {
if (typeof data === 'string') return svgdoc_.createTextNode(data);
let shape = getElem(data.attr.id);
// if shape is a path but we need to create a rect/ellipse, then remove the path
const currentLayer = jsonContext_.getDrawing().getCurrentLayer();
if (shape && data.element !== shape.tagName) {
shape.remove();
shape = null;
}
if (!shape) {
const ns = data.namespace || NS.SVG;
shape = svgdoc_.createElementNS(ns, data.element);
if (currentLayer) {
(jsonContext_.getCurrentGroup() || currentLayer).append(shape);
}
}
const curShape = jsonContext_.getCurShape();
if (data.curStyles) {
assignAttributes(shape, {
fill: curShape.fill,
stroke: curShape.stroke,
'stroke-width': curShape.stroke_width,
'stroke-dasharray': curShape.stroke_dasharray,
'stroke-linejoin': curShape.stroke_linejoin,
'stroke-linecap': curShape.stroke_linecap,
'stroke-opacity': curShape.stroke_opacity,
'fill-opacity': curShape.fill_opacity,
opacity: curShape.opacity / 2,
style: 'pointer-events:inherit'
}, 100);
}
assignAttributes(shape, data.attr, 100);
cleanupElement(shape);
// Children
if (data.children) {
data.children.forEach((child) => {
shape.append(addSVGElementsFromJson(child));
});
}
return shape;
};

132
src/svgcanvas/paste-elem.js Normal file
View File

@@ -0,0 +1,132 @@
/* globals jQuery */
import jQueryPluginSVG from '../common/jQuery.attr.js'; // Needed for SVG attribute setting and array form with `attr`
import {
getStrokedBBoxDefaultVisible
} from '../common/utilities.js';
import * as hstry from './history.js';
// Constants
const $ = jQueryPluginSVG(jQuery);
const {
InsertElementCommand, BatchCommand
} = hstry;
let pasteContext_ = null;
/**
* @function module:paste-elem.init
* @param {module:paste-elem.pasteContext} pasteContext
* @returns {void}
*/
export const init = function (pasteContext) {
pasteContext_ = pasteContext;
};
/**
* @function module:svgcanvas.SvgCanvas#pasteElements
* @param {"in_place"|"point"|void} type
* @param {Integer|void} x Expected if type is "point"
* @param {Integer|void} y Expected if type is "point"
* @fires module:svgcanvas.SvgCanvas#event:changed
* @fires module:svgcanvas.SvgCanvas#event:ext_IDsUpdated
* @returns {void}
*/
export const pasteElementsMethod = function (type, x, y) {
let clipb = JSON.parse(sessionStorage.getItem(pasteContext_.getClipBoardID()));
if (!clipb) return;
let len = clipb.length;
if (!len) return;
const pasted = [];
const batchCmd = new BatchCommand('Paste elements');
// const drawing = getCurrentDrawing();
/**
* @typedef {PlainObject<string, string>} module:svgcanvas.ChangedIDs
*/
/**
* @type {module:svgcanvas.ChangedIDs}
*/
const changedIDs = {};
// Recursively replace IDs and record the changes
/**
*
* @param {module:svgcanvas.SVGAsJSON} elem
* @returns {void}
*/
function checkIDs (elem) {
if (elem.attr && elem.attr.id) {
changedIDs[elem.attr.id] = pasteContext_.getCanvas().getNextId();
elem.attr.id = changedIDs[elem.attr.id];
}
if (elem.children) elem.children.forEach((child) => checkIDs(child));
}
clipb.forEach((elem) => checkIDs(elem));
// Give extensions like the connector extension a chance to reflect new IDs and remove invalid elements
/**
* Triggered when `pasteElements` is called from a paste action (context menu or key).
* @event module:svgcanvas.SvgCanvas#event:ext_IDsUpdated
* @type {PlainObject}
* @property {module:svgcanvas.SVGAsJSON[]} elems
* @property {module:svgcanvas.ChangedIDs} changes Maps past ID (on attribute) to current ID
*/
pasteContext_.getCanvas().runExtensions(
'IDsUpdated',
/** @type {module:svgcanvas.SvgCanvas#event:ext_IDsUpdated} */
{elems: clipb, changes: changedIDs},
true
).forEach(function (extChanges) {
if (!extChanges || !('remove' in extChanges)) return;
extChanges.remove.forEach(function (removeID) {
clipb = clipb.filter(function (clipBoardItem) {
return clipBoardItem.attr.id !== removeID;
});
});
});
// Move elements to lastClickPoint
while (len--) {
const elem = clipb[len];
if (!elem) { continue; }
const copy = pasteContext_.getCanvas().addSVGElementFromJson(elem);
pasted.push(copy);
batchCmd.addSubCommand(new InsertElementCommand(copy));
pasteContext_.restoreRefElems(copy);
}
pasteContext_.getCanvas().selectOnly(pasted);
if (type !== 'in_place') {
let ctrX, ctrY;
if (!type) {
ctrX = pasteContext_.getLastClickPoint('x');
ctrY = pasteContext_.getLastClickPoint('y');
} else if (type === 'point') {
ctrX = x;
ctrY = y;
}
const bbox = getStrokedBBoxDefaultVisible(pasted);
const cx = ctrX - (bbox.x + bbox.width / 2),
cy = ctrY - (bbox.y + bbox.height / 2),
dx = [],
dy = [];
$.each(pasted, function (i, item) {
dx.push(cx);
dy.push(cy);
});
const cmd = pasteContext_.getCanvas().moveSelectedElements(dx, dy, false);
if (cmd) batchCmd.addSubCommand(cmd);
}
pasteContext_.addCommandToHistory(batchCmd);
pasteContext_.getCanvas().call('changed', pasted);
};

File diff suppressed because it is too large Load Diff

1062
src/svgcanvas/path-method.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -269,13 +269,7 @@ export const recalculateDimensions = function (selected) {
const gangle = getRotationAngle(selected);
if (gangle) {
const a = gangle * Math.PI / 180;
let s;
if (Math.abs(a) > (1.0e-10)) {
s = Math.sin(a) / (1 - Math.cos(a));
} else {
// TODO: This blows up if the angle is exactly 0!
s = 2 / a;
}
const s = Math.abs(a) > (1.0e-10) ? Math.sin(a) / (1 - Math.cos(a)) : 2 / a;
for (let i = 0; i < tlist.numberOfItems; ++i) {
const xform = tlist.getItem(i);
if (xform.type === 4) {

View File

@@ -450,7 +450,7 @@ export class SelectorManager {
if (isNullish(elem)) { return; }
const N = this.selectors.length,
sel = this.selectorMap[elem.id];
if (!sel.locked) {
if (sel && !sel.locked) {
// TODO(codedread): Ensure this exists in this module.
console.log('WARNING! selector was released but was already unlocked'); // eslint-disable-line no-console
}

View File

@@ -0,0 +1,989 @@
/* globals jQuery */
/**
* Tools for SVG selected element operation.
* @module selected-elem
* @license MIT
*
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import jQueryPluginSVG from '../common/jQuery.attr.js'; // Needed for SVG attribute
import {NS} from '../common/namespaces.js';
import * as hstry from './history.js';
import * as pathModule from './path.js';
import {
isNullish, getStrokedBBoxDefaultVisible, setHref, getElem, getHref, getVisibleElements,
findDefs, getRotationAngle, getRefElem, getBBox as utilsGetBBox, walkTreePost, assignAttributes
} from '../common/utilities.js';
import {
transformPoint, matrixMultiply, transformListToTransform
} from '../common/math.js';
import {
getTransformList
} from '../common/svgtransformlist.js';
import {
recalculateDimensions
} from './recalculate.js';
import {
isGecko
} from '../common/browser.js'; // , supportsEditableText
const {
MoveElementCommand, BatchCommand, InsertElementCommand, RemoveElementCommand, ChangeElementCommand
} = hstry;
const $ = jQueryPluginSVG(jQuery);
let elementContext_ = null;
/**
* @function module:selected-elem.init
* @param {module:selected-elem.elementContext} elementContext
* @returns {void}
*/
export const init = function (elementContext) {
elementContext_ = elementContext;
};
/**
* Repositions the selected element to the bottom in the DOM to appear on top of
* other elements.
* @function module:selected-elem.SvgCanvas#moveToTopSelectedElem
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const moveToTopSelectedElem = function () {
const [selected] = elementContext_.getSelectedElements();
if (!isNullish(selected)) {
let t = selected;
const oldParent = t.parentNode;
const oldNextSibling = t.nextSibling;
t = t.parentNode.appendChild(t);
// If the element actually moved position, add the command and fire the changed
// event handler.
if (oldNextSibling !== t.nextSibling) {
elementContext_.addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'top'));
elementContext_.call('changed', [t]);
}
}
};
/**
* Repositions the selected element to the top in the DOM to appear under
* other elements.
* @function module:selected-elem.SvgCanvas#moveToBottomSelectedElement
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const moveToBottomSelectedElem = function () {
const [selected] = elementContext_.getSelectedElements();
if (!isNullish(selected)) {
let t = selected;
const oldParent = t.parentNode;
const oldNextSibling = t.nextSibling;
let {firstChild} = t.parentNode;
if (firstChild.tagName === 'title') {
firstChild = firstChild.nextSibling;
}
// This can probably be removed, as the defs should not ever apppear
// inside a layer group
if (firstChild.tagName === 'defs') {
firstChild = firstChild.nextSibling;
}
t = t.parentNode.insertBefore(t, firstChild);
// If the element actually moved position, add the command and fire the changed
// event handler.
if (oldNextSibling !== t.nextSibling) {
elementContext_.addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'bottom'));
elementContext_.call('changed', [t]);
}
}
};
/**
* Moves the select element up or down the stack, based on the visibly
* intersecting elements.
* @function module:selected-elem.SvgCanvas#moveUpDownSelected
* @param {"Up"|"Down"} dir - String that's either 'Up' or 'Down'
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const moveUpDownSelected = function (dir) {
const selectedElements = elementContext_.getSelectedElements();
const selected = selectedElements[0];
if (!selected) { return; }
elementContext_.setCurBBoxes([]);
// curBBoxes = [];
let closest, foundCur;
// jQuery sorts this list
const list = $(elementContext_.getIntersectionList(getStrokedBBoxDefaultVisible([selected]))).toArray();
if (dir === 'Down') { list.reverse(); }
$.each(list, function () {
if (!foundCur) {
if (this === selected) {
foundCur = true;
}
return true;
}
closest = this;
return false;
});
if (!closest) { return; }
const t = selected;
const oldParent = t.parentNode;
const oldNextSibling = t.nextSibling;
$(closest)[dir === 'Down' ? 'before' : 'after'](t);
// If the element actually moved position, add the command and fire the changed
// event handler.
if (oldNextSibling !== t.nextSibling) {
elementContext_.addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir));
elementContext_.call('changed', [t]);
}
};
/**
* Moves selected elements on the X/Y axis.
* @function module:selected-elem.SvgCanvas#moveSelectedElements
* @param {Float} dx - Float with the distance to move on the x-axis
* @param {Float} dy - Float with the distance to move on the y-axis
* @param {boolean} undoable - Boolean indicating whether or not the action should be undoable
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {BatchCommand|void} Batch command for the move
*/
export const moveSelectedElements = function (dx, dy, undoable) {
const selectedElements = elementContext_.getSelectedElements();
const currentZoom = elementContext_.getCurrentZoom();
// if undoable is not sent, default to true
// if single values, scale them to the zoom
if (dx.constructor !== Array) {
dx /= currentZoom;
dy /= currentZoom;
}
undoable = undoable || true;
const batchCmd = new BatchCommand('position');
let i = selectedElements.length;
while (i--) {
const selected = selectedElements[i];
if (!isNullish(selected)) {
// if (i === 0) {
// selectedBBoxes[0] = utilsGetBBox(selected);
// }
// const b = {};
// for (const j in selectedBBoxes[i]) b[j] = selectedBBoxes[i][j];
// selectedBBoxes[i] = b;
const xform = elementContext_.getSVGRoot().createSVGTransform();
const tlist = getTransformList(selected);
// dx and dy could be arrays
if (dx.constructor === Array) {
// if (i === 0) {
// selectedBBoxes[0].x += dx[0];
// selectedBBoxes[0].y += dy[0];
// }
xform.setTranslate(dx[i], dy[i]);
} else {
// if (i === 0) {
// selectedBBoxes[0].x += dx;
// selectedBBoxes[0].y += dy;
// }
xform.setTranslate(dx, dy);
}
if (tlist.numberOfItems) {
tlist.insertItemBefore(xform, 0);
} else {
tlist.appendItem(xform);
}
const cmd = recalculateDimensions(selected);
if (cmd) {
batchCmd.addSubCommand(cmd);
}
elementContext_.gettingSelectorManager().requestSelector(selected).resize();
}
}
if (!batchCmd.isEmpty()) {
if (undoable) {
elementContext_.addCommandToHistory(batchCmd);
}
elementContext_.call('changed', selectedElements);
return batchCmd;
}
return undefined;
};
/**
* Create deep DOM copies (clones) of all selected elements and move them slightly
* from their originals.
* @function module:selected-elem.SvgCanvas#cloneSelectedElements
* @param {Float} x Float with the distance to move on the x-axis
* @param {Float} y Float with the distance to move on the y-axis
* @returns {void}
*/
export const cloneSelectedElements = function (x, y) {
const selectedElements = elementContext_.getSelectedElements();
const currentGroup = elementContext_.getCurrentGroup();
let i, elem;
const batchCmd = new BatchCommand('Clone Elements');
// find all the elements selected (stop at first null)
const len = selectedElements.length;
/**
* Sorts an array numerically and ascending.
* @param {Element} a
* @param {Element} b
* @returns {Integer}
*/
function sortfunction (a, b) {
return ($(b).index() - $(a).index());
}
selectedElements.sort(sortfunction);
for (i = 0; i < len; ++i) {
elem = selectedElements[i];
if (isNullish(elem)) { break; }
}
// use slice to quickly get the subset of elements we need
const copiedElements = selectedElements.slice(0, i);
this.clearSelection(true);
// note that we loop in the reverse way because of the way elements are added
// to the selectedElements array (top-first)
const drawing = elementContext_.getDrawing();
i = copiedElements.length;
while (i--) {
// clone each element and replace it within copiedElements
elem = copiedElements[i] = drawing.copyElem(copiedElements[i]);
(currentGroup || drawing.getCurrentLayer()).append(elem);
batchCmd.addSubCommand(new InsertElementCommand(elem));
}
if (!batchCmd.isEmpty()) {
elementContext_.addToSelection(copiedElements.reverse()); // Need to reverse for correct selection-adding
moveSelectedElements(x, y, false);
elementContext_.addCommandToHistory(batchCmd);
}
};
/**
* Aligns selected elements.
* @function module:selected-elem.SvgCanvas#alignSelectedElements
* @param {string} type - String with single character indicating the alignment type
* @param {"selected"|"largest"|"smallest"|"page"} relativeTo
* @returns {void}
*/
export const alignSelectedElements = function (type, relativeTo) {
const selectedElements = elementContext_.getSelectedElements();
const bboxes = []; // angles = [];
const len = selectedElements.length;
if (!len) { return; }
let minx = Number.MAX_VALUE, maxx = Number.MIN_VALUE,
miny = Number.MAX_VALUE, maxy = Number.MIN_VALUE;
let curwidth = Number.MIN_VALUE, curheight = Number.MIN_VALUE;
for (let i = 0; i < len; ++i) {
if (isNullish(selectedElements[i])) { break; }
const elem = selectedElements[i];
bboxes[i] = getStrokedBBoxDefaultVisible([elem]);
// now bbox is axis-aligned and handles rotation
switch (relativeTo) {
case 'smallest':
if (((type === 'l' || type === 'c' || type === 'r') &&
(curwidth === Number.MIN_VALUE || curwidth > bboxes[i].width)) ||
((type === 't' || type === 'm' || type === 'b') &&
(curheight === Number.MIN_VALUE || curheight > bboxes[i].height))
) {
minx = bboxes[i].x;
miny = bboxes[i].y;
maxx = bboxes[i].x + bboxes[i].width;
maxy = bboxes[i].y + bboxes[i].height;
curwidth = bboxes[i].width;
curheight = bboxes[i].height;
}
break;
case 'largest':
if (((type === 'l' || type === 'c' || type === 'r') &&
(curwidth === Number.MIN_VALUE || curwidth < bboxes[i].width)) ||
((type === 't' || type === 'm' || type === 'b') &&
(curheight === Number.MIN_VALUE || curheight < bboxes[i].height))
) {
minx = bboxes[i].x;
miny = bboxes[i].y;
maxx = bboxes[i].x + bboxes[i].width;
maxy = bboxes[i].y + bboxes[i].height;
curwidth = bboxes[i].width;
curheight = bboxes[i].height;
}
break;
default: // 'selected'
if (bboxes[i].x < minx) { minx = bboxes[i].x; }
if (bboxes[i].y < miny) { miny = bboxes[i].y; }
if (bboxes[i].x + bboxes[i].width > maxx) { maxx = bboxes[i].x + bboxes[i].width; }
if (bboxes[i].y + bboxes[i].height > maxy) { maxy = bboxes[i].y + bboxes[i].height; }
break;
}
} // loop for each element to find the bbox and adjust min/max
if (relativeTo === 'page') {
minx = 0;
miny = 0;
maxx = elementContext_.getContentW();
maxy = elementContext_.getContentH();
}
const dx = new Array(len);
const dy = new Array(len);
for (let i = 0; i < len; ++i) {
if (isNullish(selectedElements[i])) { break; }
// const elem = selectedElements[i];
const bbox = bboxes[i];
dx[i] = 0;
dy[i] = 0;
switch (type) {
case 'l': // left (horizontal)
dx[i] = minx - bbox.x;
break;
case 'c': // center (horizontal)
dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2);
break;
case 'r': // right (horizontal)
dx[i] = maxx - (bbox.x + bbox.width);
break;
case 't': // top (vertical)
dy[i] = miny - bbox.y;
break;
case 'm': // middle (vertical)
dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2);
break;
case 'b': // bottom (vertical)
dy[i] = maxy - (bbox.y + bbox.height);
break;
}
}
moveSelectedElements(dx, dy);
};
/**
* Removes all selected elements from the DOM and adds the change to the
* history stack.
* @function module:selected-elem.SvgCanvas#deleteSelectedElements
* @fires module:selected-elem.SvgCanvas#event:changed
* @returns {void}
*/
export const deleteSelectedElements = function () {
const selectedElements = elementContext_.getSelectedElements();
const batchCmd = new BatchCommand('Delete Elements');
const len = selectedElements.length;
const selectedCopy = []; // selectedElements is being deleted
for (let i = 0; i < len; ++i) {
const selected = selectedElements[i];
if (isNullish(selected)) { break; }
let parent = selected.parentNode;
let t = selected;
// this will unselect the element and remove the selectedOutline
elementContext_.gettingSelectorManager().releaseSelector(t);
// Remove the path if present.
pathModule.removePath_(t.id);
// Get the parent if it's a single-child anchor
if (parent.tagName === 'a' && parent.childNodes.length === 1) {
t = parent;
parent = parent.parentNode;
}
const {nextSibling} = t;
t.remove();
const elem = t;
selectedCopy.push(selected); // for the copy
batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));
}
elementContext_.getCanvas().setEmptySelectedElements();
if (!batchCmd.isEmpty()) { elementContext_.addCommandToHistory(batchCmd); }
elementContext_.call('changed', selectedCopy);
elementContext_.clearSelection();
};
/**
* Remembers the current selected elements on the clipboard.
* @function module:selected-elem.SvgCanvas#copySelectedElements
* @returns {void}
*/
export const copySelectedElements = function () {
const selectedElements = elementContext_.getSelectedElements();
const data =
JSON.stringify(selectedElements.map((x) => elementContext_.getJsonFromSvgElement(x)));
// Use sessionStorage for the clipboard data.
sessionStorage.setItem(elementContext_.getClipboardID(), data);
elementContext_.flashStorage();
const menu = $('#cmenu_canvas');
// Context menu might not exist (it is provided by editor.js).
if (menu.enableContextMenuItems) {
menu.enableContextMenuItems('#paste,#paste_in_place');
}
};
/**
* Wraps all the selected elements in a group (`g`) element.
* @function module:selected-elem.SvgCanvas#groupSelectedElements
* @param {"a"|"g"} [type="g"] - type of element to group into, defaults to `<g>`
* @param {string} [urlArg]
* @returns {void}
*/
export const groupSelectedElements = function (type, urlArg) {
const selectedElements = elementContext_.getSelectedElements();
if (!type) { type = 'g'; }
let cmdStr = '';
let url;
switch (type) {
case 'a': {
cmdStr = 'Make hyperlink';
url = urlArg || '';
break;
} default: {
type = 'g';
cmdStr = 'Group Elements';
break;
}
}
const batchCmd = new BatchCommand(cmdStr);
// create and insert the group element
const g = elementContext_.addSVGElementFromJson({
element: type,
attr: {
id: elementContext_.getNextId()
}
});
if (type === 'a') {
setHref(g, url);
}
batchCmd.addSubCommand(new InsertElementCommand(g));
// now move all children into the group
let i = selectedElements.length;
while (i--) {
let elem = selectedElements[i];
if (isNullish(elem)) { continue; }
if (elem.parentNode.tagName === 'a' && elem.parentNode.childNodes.length === 1) {
elem = elem.parentNode;
}
const oldNextSibling = elem.nextSibling;
const oldParent = elem.parentNode;
g.append(elem);
batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));
}
if (!batchCmd.isEmpty()) { elementContext_.addCommandToHistory(batchCmd); }
// update selection
elementContext_.selectOnly([g], true);
};
/**
* Pushes all appropriate parent group properties down to its children, then
* removes them from the group.
* @function module:selected-elem.SvgCanvas#pushGroupProperty
* @param {SVGAElement|SVGGElement} g
* @param {boolean} undoable
* @returns {BatchCommand|void}
*/
export const pushGroupProperty = function (g, undoable) {
const children = g.childNodes;
const len = children.length;
const xform = g.getAttribute('transform');
const glist = getTransformList(g);
const m = transformListToTransform(glist).matrix;
const batchCmd = new BatchCommand('Push group properties');
// TODO: get all fill/stroke properties from the group that we are about to destroy
// "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
// "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
// "stroke-width"
// and then for each child, if they do not have the attribute (or the value is 'inherit')
// then set the child's attribute
const gangle = getRotationAngle(g);
const gattrs = $(g).attr(['filter', 'opacity']);
let gfilter, gblur, changes;
const drawing = elementContext_.getDrawing();
for (let i = 0; i < len; i++) {
const elem = children[i];
if (elem.nodeType !== 1) { continue; }
if (gattrs.opacity !== null && gattrs.opacity !== 1) {
// const c_opac = elem.getAttribute('opacity') || 1;
const newOpac = Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100) / 100;
elementContext_.changeSelectedAttribute('opacity', newOpac, [elem]);
}
if (gattrs.filter) {
let cblur = this.getBlur(elem);
const origCblur = cblur;
if (!gblur) { gblur = this.getBlur(g); }
if (cblur) {
// Is this formula correct?
cblur = Number(gblur) + Number(cblur);
} else if (cblur === 0) {
cblur = gblur;
}
// If child has no current filter, get group's filter or clone it.
if (!origCblur) {
// Set group's filter to use first child's ID
if (!gfilter) {
gfilter = getRefElem(gattrs.filter);
} else {
// Clone the group's filter
gfilter = drawing.copyElem(gfilter);
findDefs().append(gfilter);
}
} else {
gfilter = getRefElem(elem.getAttribute('filter'));
}
// Change this in future for different filters
const suffix = (gfilter.firstChild.tagName === 'feGaussianBlur') ? 'blur' : 'filter';
gfilter.id = elem.id + '_' + suffix;
elementContext_.changeSelectedAttribute('filter', 'url(#' + gfilter.id + ')', [elem]);
// Update blur value
if (cblur) {
elementContext_.changeSelectedAttribute('stdDeviation', cblur, [gfilter.firstChild]);
elementContext_.getCanvas().setBlurOffsets(gfilter, cblur);
}
}
let chtlist = getTransformList(elem);
// Don't process gradient transforms
if (elem.tagName.includes('Gradient')) { chtlist = null; }
// Hopefully not a problem to add this. Necessary for elements like <desc/>
if (!chtlist) { continue; }
// Apparently <defs> can get get a transformlist, but we don't want it to have one!
if (elem.tagName === 'defs') { continue; }
if (glist.numberOfItems) {
// TODO: if the group's transform is just a rotate, we can always transfer the
// rotate() down to the children (collapsing consecutive rotates and factoring
// out any translates)
if (gangle && glist.numberOfItems === 1) {
// [Rg] [Rc] [Mc]
// we want [Tr] [Rc2] [Mc] where:
// - [Rc2] is at the child's current center but has the
// sum of the group and child's rotation angles
// - [Tr] is the equivalent translation that this child
// undergoes if the group wasn't there
// [Tr] = [Rg] [Rc] [Rc2_inv]
// get group's rotation matrix (Rg)
const rgm = glist.getItem(0).matrix;
// get child's rotation matrix (Rc)
let rcm = elementContext_.getSVGRoot().createSVGMatrix();
const cangle = getRotationAngle(elem);
if (cangle) {
rcm = chtlist.getItem(0).matrix;
}
// get child's old center of rotation
const cbox = utilsGetBBox(elem);
const ceqm = transformListToTransform(chtlist).matrix;
const coldc = transformPoint(cbox.x + cbox.width / 2, cbox.y + cbox.height / 2, ceqm);
// sum group and child's angles
const sangle = gangle + cangle;
// get child's rotation at the old center (Rc2_inv)
const r2 = elementContext_.getSVGRoot().createSVGTransform();
r2.setRotate(sangle, coldc.x, coldc.y);
// calculate equivalent translate
const trm = matrixMultiply(rgm, rcm, r2.matrix.inverse());
// set up tlist
if (cangle) {
chtlist.removeItem(0);
}
if (sangle) {
if (chtlist.numberOfItems) {
chtlist.insertItemBefore(r2, 0);
} else {
chtlist.appendItem(r2);
}
}
if (trm.e || trm.f) {
const tr = elementContext_.getSVGRoot().createSVGTransform();
tr.setTranslate(trm.e, trm.f);
if (chtlist.numberOfItems) {
chtlist.insertItemBefore(tr, 0);
} else {
chtlist.appendItem(tr);
}
}
} else { // more complicated than just a rotate
// transfer the group's transform down to each child and then
// call recalculateDimensions()
const oldxform = elem.getAttribute('transform');
changes = {};
changes.transform = oldxform || '';
const newxform = elementContext_.getSVGRoot().createSVGTransform();
// [ gm ] [ chm ] = [ chm ] [ gm' ]
// [ gm' ] = [ chmInv ] [ gm ] [ chm ]
const chm = transformListToTransform(chtlist).matrix,
chmInv = chm.inverse();
const gm = matrixMultiply(chmInv, m, chm);
newxform.setMatrix(gm);
chtlist.appendItem(newxform);
}
const cmd = recalculateDimensions(elem);
if (cmd) { batchCmd.addSubCommand(cmd); }
}
}
// remove transform and make it undo-able
if (xform) {
changes = {};
changes.transform = xform;
g.setAttribute('transform', '');
g.removeAttribute('transform');
batchCmd.addSubCommand(new ChangeElementCommand(g, changes));
}
if (undoable && !batchCmd.isEmpty()) {
return batchCmd;
}
return undefined;
};
/**
* Converts selected/given `<use>` or child SVG element to a group.
* @function module:selected-elem.SvgCanvas#convertToGroup
* @param {Element} elem
* @fires module:selected-elem.SvgCanvas#event:selected
* @returns {void}
*/
export const convertToGroup = function (elem) {
const selectedElements = elementContext_.getSelectedElements();
if (!elem) {
elem = selectedElements[0];
}
const $elem = $(elem);
const batchCmd = new BatchCommand();
let ts;
if ($elem.data('gsvg')) {
// Use the gsvg as the new group
const svg = elem.firstChild;
const pt = $(svg).attr(['x', 'y']);
$(elem.firstChild.firstChild).unwrap();
$(elem).removeData('gsvg');
const tlist = getTransformList(elem);
const xform = elementContext_.getSVGRoot().createSVGTransform();
xform.setTranslate(pt.x, pt.y);
tlist.appendItem(xform);
recalculateDimensions(elem);
elementContext_.call('selected', [elem]);
} else if ($elem.data('symbol')) {
elem = $elem.data('symbol');
ts = $elem.attr('transform');
const pos = $elem.attr(['x', 'y']);
const vb = elem.getAttribute('viewBox');
if (vb) {
const nums = vb.split(' ');
pos.x -= Number(nums[0]);
pos.y -= Number(nums[1]);
}
// Not ideal, but works
ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')';
const prev = $elem.prev();
// Remove <use> element
batchCmd.addSubCommand(new RemoveElementCommand($elem[0], $elem[0].nextSibling, $elem[0].parentNode));
$elem.remove();
// See if other elements reference this symbol
const svgcontent = elementContext_.getSVGContent();
const hasMore = $(svgcontent).find('use:data(symbol)').length;
const g = elementContext_.getDOMDocument().createElementNS(NS.SVG, 'g');
const childs = elem.childNodes;
let i;
for (i = 0; i < childs.length; i++) {
g.append(childs[i].cloneNode(true));
}
// Duplicate the gradients for Gecko, since they weren't included in the <symbol>
if (isGecko()) {
const dupeGrads = $(findDefs()).children('linearGradient,radialGradient,pattern').clone();
$(g).append(dupeGrads);
}
if (ts) {
g.setAttribute('transform', ts);
}
const parent = elem.parentNode;
elementContext_.uniquifyElems(g);
// Put the dupe gradients back into <defs> (after uniquifying them)
if (isGecko()) {
$(findDefs()).append($(g).find('linearGradient,radialGradient,pattern'));
}
// now give the g itself a new id
g.id = elementContext_.getNextId();
prev.after(g);
if (parent) {
if (!hasMore) {
// remove symbol/svg element
const {nextSibling} = elem;
elem.remove();
batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));
}
batchCmd.addSubCommand(new InsertElementCommand(g));
}
elementContext_.setUseData(g);
if (isGecko()) {
elementContext_.convertGradients(findDefs());
} else {
elementContext_.convertGradients(g);
}
// recalculate dimensions on the top-level children so that unnecessary transforms
// are removed
walkTreePost(g, function (n) {
try {
recalculateDimensions(n);
} catch (e) {
console.log(e); // eslint-disable-line no-console
}
});
// Give ID for any visible element missing one
const visElems = elementContext_.getVisElems();
$(g).find(visElems).each(function () {
if (!this.id) { this.id = elementContext_.getNextId(); }
});
elementContext_.selectOnly([g]);
const cm = pushGroupProperty(g, true);
if (cm) {
batchCmd.addSubCommand(cm);
}
elementContext_.addCommandToHistory(batchCmd);
} else {
console.log('Unexpected element to ungroup:', elem); // eslint-disable-line no-console
}
};
/**
* Unwraps all the elements in a selected group (`g`) element. This requires
* significant recalculations to apply group's transforms, etc. to its children.
* @function module:selected-elem.SvgCanvas#ungroupSelectedElement
* @returns {void}
*/
export const ungroupSelectedElement = function () {
const selectedElements = elementContext_.getSelectedElements();
let g = selectedElements[0];
if (!g) {
return;
}
if ($(g).data('gsvg') || $(g).data('symbol')) {
// Is svg, so actually convert to group
convertToGroup(g);
return;
}
if (g.tagName === 'use') {
// Somehow doesn't have data set, so retrieve
const symbol = getElem(getHref(g).substr(1));
$(g).data('symbol', symbol).data('ref', symbol);
convertToGroup(g);
return;
}
const parentsA = $(g).parents('a');
if (parentsA.length) {
g = parentsA[0];
}
// Look for parent "a"
if (g.tagName === 'g' || g.tagName === 'a') {
const batchCmd = new BatchCommand('Ungroup Elements');
const cmd = pushGroupProperty(g, true);
if (cmd) { batchCmd.addSubCommand(cmd); }
const parent = g.parentNode;
const anchor = g.nextSibling;
const children = new Array(g.childNodes.length);
let i = 0;
while (g.firstChild) {
const elem = g.firstChild;
const oldNextSibling = elem.nextSibling;
const oldParent = elem.parentNode;
// Remove child title elements
if (elem.tagName === 'title') {
const {nextSibling} = elem;
batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, oldParent));
elem.remove();
continue;
}
if (anchor) {
anchor.before(elem);
} else {
g.after(elem);
}
children[i++] = elem;
batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));
}
// remove the group from the selection
elementContext_.clearSelection();
// delete the group element (but make undo-able)
const gNextSibling = g.nextSibling;
g.remove();
batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent));
if (!batchCmd.isEmpty()) { elementContext_.addCommandToHistory(batchCmd); }
// update selection
elementContext_.addToSelection(children);
}
};
/**
* Updates the editor canvas width/height/position after a zoom has occurred.
* @function module:svgcanvas.SvgCanvas#updateCanvas
* @param {Float} w - Float with the new width
* @param {Float} h - Float with the new height
* @fires module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
* @returns {module:svgcanvas.CanvasInfo}
*/
export const updateCanvas = function (w, h) {
elementContext_.getSVGRoot().setAttribute('width', w);
elementContext_.getSVGRoot().setAttribute('height', h);
const currentZoom = elementContext_.getCurrentZoom();
const bg = $('#canvasBackground')[0];
const oldX = elementContext_.getSVGContent().getAttribute('x');
const oldY = elementContext_.getSVGContent().getAttribute('y');
const x = ((w - this.contentW * currentZoom) / 2);
const y = ((h - this.contentH * currentZoom) / 2);
assignAttributes(elementContext_.getSVGContent(), {
width: this.contentW * currentZoom,
height: this.contentH * currentZoom,
x,
y,
viewBox: '0 0 ' + this.contentW + ' ' + this.contentH
});
assignAttributes(bg, {
width: elementContext_.getSVGContent().getAttribute('width'),
height: elementContext_.getSVGContent().getAttribute('height'),
x,
y
});
const bgImg = getElem('background_image');
if (bgImg) {
assignAttributes(bgImg, {
width: '100%',
height: '100%'
});
}
elementContext_.getCanvas().selectorManager.selectorParentGroup.setAttribute('transform', 'translate(' + x + ',' + y + ')');
/**
* Invoked upon updates to the canvas.
* @event module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
* @type {PlainObject}
* @property {Integer} new_x
* @property {Integer} new_y
* @property {string} old_x (Of Integer)
* @property {string} old_y (Of Integer)
* @property {Integer} d_x
* @property {Integer} d_y
*/
elementContext_.getCanvas().runExtensions(
'canvasUpdated',
/**
* @type {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated}
*/
{new_x: x, new_y: y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY}
);
return {x, y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY};
};
/**
* Select the next/previous element within the current layer.
* @function module:svgcanvas.SvgCanvas#cycleElement
* @param {boolean} next - true = next and false = previous element
* @fires module:svgcanvas.SvgCanvas#event:selected
* @returns {void}
*/
export const cycleElement = function (next) {
const selectedElements = elementContext_.getSelectedElements();
const currentGroup = elementContext_.getCurrentGroup();
let num;
const curElem = selectedElements[0];
let elem = false;
const allElems = getVisibleElements(currentGroup || elementContext_.getCanvas().getCurrentDrawing().getCurrentLayer());
if (!allElems.length) { return; }
if (isNullish(curElem)) {
num = next ? allElems.length - 1 : 0;
elem = allElems[num];
} else {
let i = allElems.length;
while (i--) {
if (allElems[i] === curElem) {
num = next ? i - 1 : i + 1;
if (num >= allElems.length) {
num = 0;
} else if (num < 0) {
num = allElems.length - 1;
}
elem = allElems[num];
break;
}
}
}
elementContext_.getCanvas().selectOnly([elem], true);
elementContext_.call('selected', selectedElements);
};

444
src/svgcanvas/selection.js Normal file
View File

@@ -0,0 +1,444 @@
/* globals jQuery */
/**
* Tools for selection.
* @module selection
* @license MIT
* @copyright 2011 Jeff Schiller
*/
import {NS} from '../common/namespaces.js';
import {
isNullish, getBBox as utilsGetBBox, getStrokedBBoxDefaultVisible
} from '../common/utilities.js';
import {transformPoint, transformListToTransform, rectsIntersect} from '../common/math.js';
import jQueryPluginSVG from '../common/jQuery.attr.js';
import {
getTransformList
} from '../common/svgtransformlist.js';
import * as hstry from './history.js';
const {BatchCommand} = hstry;
const $ = jQueryPluginSVG(jQuery);
let selectionContext_ = null;
/**
* @function module:selection.init
* @param {module:selection.selectionContext} selectionContext
* @returns {void}
*/
export const init = function (selectionContext) {
selectionContext_ = selectionContext;
};
/**
* Clears the selection. The 'selected' handler is then optionally called.
* This should really be an intersection applying to all types rather than a union.
* @name module:selection.SvgCanvas#clearSelection
* @type {module:draw.DrawCanvasInit#clearSelection|module:path.EditorContext#clearSelection}
* @fires module:selection.SvgCanvas#event:selected
*/
export const clearSelectionMethod = function (noCall) {
const selectedElements = selectionContext_.getSelectedElements();
selectedElements.forEach((elem) => {
if (isNullish(elem)) {
return;
}
selectionContext_.getCanvas().selectorManager.releaseSelector(elem);
});
selectionContext_.getCanvas().setEmptySelectedElements();
if (!noCall) { selectionContext_.getCanvas().call('selected', selectionContext_.getSelectedElements()); }
};
/**
* Adds a list of elements to the selection. The 'selected' handler is then called.
* @name module:selection.SvgCanvas#addToSelection
* @type {module:path.EditorContext#addToSelection}
* @fires module:selection.SvgCanvas#event:selected
*/
export const addToSelectionMethod = function (elemsToAdd, showGrips) {
const selectedElements = selectionContext_.getSelectedElements();
if (!elemsToAdd.length) { return; }
// find the first null in our selectedElements array
let j = 0;
while (j < selectedElements.length) {
if (isNullish(selectedElements[j])) {
break;
}
++j;
}
// now add each element consecutively
let i = elemsToAdd.length;
while (i--) {
let elem = elemsToAdd[i];
if (!elem) { continue; }
const bbox = utilsGetBBox(elem);
if (!bbox) { continue; }
if (elem.tagName === 'a' && elem.childNodes.length === 1) {
// Make "a" element's child be the selected element
elem = elem.firstChild;
}
// if it's not already there, add it
if (!selectedElements.includes(elem)) {
selectedElements[j] = elem;
// only the first selectedBBoxes element is ever used in the codebase these days
// if (j === 0) selectedBBoxes[0] = utilsGetBBox(elem);
j++;
const sel = selectionContext_.getCanvas().selectorManager.requestSelector(elem, bbox);
if (selectedElements.length > 1) {
sel.showGrips(false);
}
}
}
if (!selectedElements.length) {
return;
}
selectionContext_.getCanvas().call('selected', selectedElements);
if (selectedElements.length === 1) {
selectionContext_.getCanvas().selectorManager.requestSelector(selectedElements[0]).showGrips(showGrips);
}
// make sure the elements are in the correct order
// See: https://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition
selectedElements.sort(function (a, b) {
if (a && b && a.compareDocumentPosition) {
return 3 - (b.compareDocumentPosition(a) & 6); // eslint-disable-line no-bitwise
}
if (isNullish(a)) {
return 1;
}
return 0;
});
// Make sure first elements are not null
while (isNullish(selectedElements[0])) {
selectedElements.shift(0);
}
};
/**
* @name module:svgcanvas.SvgCanvas#getMouseTarget
* @type {module:path.EditorContext#getMouseTarget}
*/
export const getMouseTargetMethod = function (evt) {
if (isNullish(evt)) {
return null;
}
let mouseTarget = evt.target;
// if it was a <use>, Opera and WebKit return the SVGElementInstance
if (mouseTarget.correspondingUseElement) { mouseTarget = mouseTarget.correspondingUseElement; }
// for foreign content, go up until we find the foreignObject
// WebKit browsers set the mouse target to the svgcanvas div
if ([NS.MATH, NS.HTML].includes(mouseTarget.namespaceURI) &&
mouseTarget.id !== 'svgcanvas'
) {
while (mouseTarget.nodeName !== 'foreignObject') {
mouseTarget = mouseTarget.parentNode;
if (!mouseTarget) { return selectionContext_.getSVGRoot(); }
}
}
// Get the desired mouseTarget with jQuery selector-fu
// If it's root-like, select the root
const currentLayer = selectionContext_.getCanvas().getCurrentDrawing().getCurrentLayer();
const svgRoot = selectionContext_.getSVGRoot();
const container = selectionContext_.getDOMContainer();
const content = selectionContext_.getSVGContent();
if ([svgRoot, container, content, currentLayer].includes(mouseTarget)) {
return selectionContext_.getSVGRoot();
}
const $target = $(mouseTarget);
// If it's a selection grip, return the grip parent
if ($target.closest('#selectorParentGroup').length) {
// While we could instead have just returned mouseTarget,
// this makes it easier to indentify as being a selector grip
return selectionContext_.getCanvas().selectorManager.selectorParentGroup;
}
while (mouseTarget.parentNode !== (selectionContext_.getCurrentGroup() || currentLayer)) {
mouseTarget = mouseTarget.parentNode;
}
//
// // go up until we hit a child of a layer
// while (mouseTarget.parentNode.parentNode.tagName == 'g') {
// mouseTarget = mouseTarget.parentNode;
// }
// Webkit bubbles the mouse event all the way up to the div, so we
// set the mouseTarget to the svgroot like the other browsers
// if (mouseTarget.nodeName.toLowerCase() == 'div') {
// mouseTarget = svgroot;
// }
return mouseTarget;
};
/**
* @typedef {module:svgcanvas.ExtensionMouseDownStatus|module:svgcanvas.ExtensionMouseUpStatus|module:svgcanvas.ExtensionIDsUpdatedStatus|module:locale.ExtensionLocaleData[]|void} module:svgcanvas.ExtensionStatus
* @tutorial ExtensionDocs
*/
/**
* @callback module:svgcanvas.ExtensionVarBuilder
* @param {string} name The name of the extension
* @returns {module:svgcanvas.SvgCanvas#event:ext_addLangData}
*/
/**
* @callback module:svgcanvas.ExtensionNameFilter
* @param {string} name
* @returns {boolean}
*/
/**
* @todo Consider: Should this return an array by default, so extension results aren't overwritten?
* @todo Would be easier to document if passing in object with key of action and vars as value; could then define an interface which tied both together
* @function module:svgcanvas.SvgCanvas#runExtensions
* @param {"mouseDown"|"mouseMove"|"mouseUp"|"zoomChanged"|"IDsUpdated"|"canvasUpdated"|"toolButtonStateUpdate"|"selectedChanged"|"elementTransition"|"elementChanged"|"langReady"|"langChanged"|"addLangData"|"onNewDocument"|"workareaResized"} action
* @param {module:svgcanvas.SvgCanvas#event:ext_mouseDown|module:svgcanvas.SvgCanvas#event:ext_mouseMove|module:svgcanvas.SvgCanvas#event:ext_mouseUp|module:svgcanvas.SvgCanvas#event:ext_zoomChanged|module:svgcanvas.SvgCanvas#event:ext_IDsUpdated|module:svgcanvas.SvgCanvas#event:ext_canvasUpdated|module:svgcanvas.SvgCanvas#event:ext_toolButtonStateUpdate|module:svgcanvas.SvgCanvas#event:ext_selectedChanged|module:svgcanvas.SvgCanvas#event:ext_elementTransition|module:svgcanvas.SvgCanvas#event:ext_elementChanged|module:svgcanvas.SvgCanvas#event:ext_langReady|module:svgcanvas.SvgCanvas#event:ext_langChanged|module:svgcanvas.SvgCanvas#event:ext_addLangData|module:svgcanvas.SvgCanvas#event:ext_onNewDocument|module:svgcanvas.SvgCanvas#event:ext_workareaResized|module:svgcanvas.ExtensionVarBuilder} [vars]
* @param {boolean} [returnArray]
* @param {module:svgcanvas.ExtensionNameFilter} nameFilter
* @returns {GenericArray<module:svgcanvas.ExtensionStatus>|module:svgcanvas.ExtensionStatus|false} See {@tutorial ExtensionDocs} on the ExtensionStatus.
*/
export const runExtensionsMethod = function (action, vars, returnArray, nameFilter) {
let result = returnArray ? [] : false;
$.each(selectionContext_.getExtensions(), function (name, ext) {
if (nameFilter && !nameFilter(name)) {
return;
}
if (ext && action in ext) {
if (typeof vars === 'function') {
vars = vars(name); // ext, action
}
if (returnArray) {
result.push(ext[action](vars));
} else {
result = ext[action](vars);
}
}
});
return result;
};
/**
* Get all elements that have a BBox (excludes `<defs>`, `<title>`, etc).
* Note that 0-opacity, off-screen etc elements are still considered "visible"
* for this function.
* @function module:svgcanvas.SvgCanvas#getVisibleElementsAndBBoxes
* @param {Element} parent - The parent DOM element to search within
* @returns {ElementAndBBox[]} An array with objects that include:
*/
export const getVisibleElementsAndBBoxes = function (parent) {
if (!parent) {
parent = $(selectionContext_.getSVGContent()).children(); // Prevent layers from being included
}
const contentElems = [];
$(parent).children().each(function (i, elem) {
if (elem.getBBox) {
contentElems.push({elem, bbox: getStrokedBBoxDefaultVisible([elem])});
}
});
return contentElems.reverse();
};
/**
* This method sends back an array or a NodeList full of elements that
* intersect the multi-select rubber-band-box on the currentLayer only.
*
* We brute-force `getIntersectionList` for browsers that do not support it (Firefox).
*
* Reference:
* Firefox does not implement `getIntersectionList()`, see {@link https://bugzilla.mozilla.org/show_bug.cgi?id=501421}.
* @function module:svgcanvas.SvgCanvas#getIntersectionList
* @param {SVGRect} rect
* @returns {Element[]|NodeList} Bbox elements
*/
export const getIntersectionListMethod = function (rect) {
const currentZoom = selectionContext_.getCurrentZoom();
if (isNullish(selectionContext_.getRubberBox())) { return null; }
const parent = selectionContext_.getCurrentGroup() || selectionContext_.getCanvas().getCurrentDrawing().getCurrentLayer();
let rubberBBox;
if (!rect) {
rubberBBox = selectionContext_.getRubberBox().getBBox();
const bb = selectionContext_.getSVGContent().createSVGRect();
['x', 'y', 'width', 'height', 'top', 'right', 'bottom', 'left'].forEach((o) => {
bb[o] = rubberBBox[o] / currentZoom;
});
rubberBBox = bb;
} else {
rubberBBox = selectionContext_.getSVGContent().createSVGRect();
rubberBBox.x = rect.x;
rubberBBox.y = rect.y;
rubberBBox.width = rect.width;
rubberBBox.height = rect.height;
}
let resultList = null;
if (!selectionContext_.isIE()) {
if (typeof selectionContext_.getSVGRoot().getIntersectionList === 'function') {
// Offset the bbox of the rubber box by the offset of the svgcontent element.
rubberBBox.x += Number.parseInt(selectionContext_.getSVGContent().getAttribute('x'));
rubberBBox.y += Number.parseInt(selectionContext_.getSVGContent().getAttribute('y'));
resultList = selectionContext_.getSVGRoot().getIntersectionList(rubberBBox, parent);
}
}
if (isNullish(resultList) || typeof resultList.item !== 'function') {
resultList = [];
if (!selectionContext_.getCurBBoxes().length) {
// Cache all bboxes
selectionContext_.setCurBBoxes(getVisibleElementsAndBBoxes(parent));
}
let i = selectionContext_.getCurBBoxes().length;
while (i--) {
const curBBoxes = selectionContext_.getCurBBoxes();
if (!rubberBBox.width) { continue; }
if (rectsIntersect(rubberBBox, curBBoxes[i].bbox)) {
resultList.push(curBBoxes[i].elem);
}
}
}
// addToSelection expects an array, but it's ok to pass a NodeList
// because using square-bracket notation is allowed:
// https://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html
return resultList;
};
/**
* @typedef {PlainObject} ElementAndBBox
* @property {Element} elem - The element
* @property {module:utilities.BBoxObject} bbox - The element's BBox as retrieved from `getStrokedBBoxDefaultVisible`
*/
/**
* Wrap an SVG element into a group element, mark the group as 'gsvg'.
* @function module:svgcanvas.SvgCanvas#groupSvgElem
* @param {Element} elem - SVG element to wrap
* @returns {void}
*/
export const groupSvgElem = function (elem) {
const g = document.createElementNS(NS.SVG, 'g');
elem.replaceWith(g);
$(g).append(elem).data('gsvg', elem)[0].id = selectionContext_.getCanvas().getNextId();
};
/**
* Runs the SVG Document through the sanitizer and then updates its paths.
* @function module:svgcanvas.SvgCanvas#prepareSvg
* @param {XMLDocument} newDoc - The SVG DOM document
* @returns {void}
*/
export const prepareSvg = function (newDoc) {
selectionContext_.getCanvas().sanitizeSvg(newDoc.documentElement);
// convert paths into absolute commands
const paths = [...newDoc.getElementsByTagNameNS(NS.SVG, 'path')];
paths.forEach((path) => {
path.setAttribute('d', selectionContext_.getCanvas().pathActions.convertPath(path));
selectionContext_.getCanvas().pathActions.fixEnd(path);
});
};
// `this.each` is deprecated, if any extension used this it can be recreated by doing this:
// * @example $(canvas.getRootElem()).children().each(...)
// * @function module:svgcanvas.SvgCanvas#each
// this.each = function (cb) {
// $(svgroot).children().each(cb);
// };
/**
* Removes any old rotations if present, prepends a new rotation at the
* transformed center.
* @function module:svgcanvas.SvgCanvas#setRotationAngle
* @param {string|Float} val - The new rotation angle in degrees
* @param {boolean} preventUndo - Indicates whether the action should be undoable or not
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
export const setRotationAngle = function (val, preventUndo) {
const selectedElements = selectionContext_.getSelectedElements();
// ensure val is the proper type
val = Number.parseFloat(val);
const elem = selectedElements[0];
const oldTransform = elem.getAttribute('transform');
const bbox = utilsGetBBox(elem);
const cx = bbox.x + bbox.width / 2, cy = bbox.y + bbox.height / 2;
const tlist = getTransformList(elem);
// only remove the real rotational transform if present (i.e. at index=0)
if (tlist.numberOfItems > 0) {
const xform = tlist.getItem(0);
if (xform.type === 4) {
tlist.removeItem(0);
}
}
// find Rnc and insert it
if (val !== 0) {
const center = transformPoint(cx, cy, transformListToTransform(tlist).matrix);
const Rnc = selectionContext_.getSVGRoot().createSVGTransform();
Rnc.setRotate(val, center.x, center.y);
if (tlist.numberOfItems) {
tlist.insertItemBefore(Rnc, 0);
} else {
tlist.appendItem(Rnc);
}
} else if (tlist.numberOfItems === 0) {
elem.removeAttribute('transform');
}
if (!preventUndo) {
// we need to undo it, then redo it so it can be undo-able! :)
// TODO: figure out how to make changes to transform list undo-able cross-browser?
const newTransform = elem.getAttribute('transform');
elem.setAttribute('transform', oldTransform);
selectionContext_.getCanvas().changeSelectedAttribute('transform', newTransform, selectedElements);
selectionContext_.getCanvas().call('changed', selectedElements);
}
// const pointGripContainer = getElem('pathpointgrip_container');
// if (elem.nodeName === 'path' && pointGripContainer) {
// pathActions.setPointContainerTransform(elem.getAttribute('transform'));
// }
const selector = selectionContext_.getCanvas().selectorManager.requestSelector(selectedElements[0]);
selector.resize();
selectionContext_.getSelector().updateGripCursors(val);
};
/**
* Runs `recalculateDimensions` on the selected elements,
* adding the changes to a single batch command.
* @function module:svgcanvas.SvgCanvas#recalculateAllSelectedDimensions
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
export const recalculateAllSelectedDimensions = function () {
const selectedElements = selectionContext_.getSelectedElements();
const text = (selectionContext_.getCurrentResizeMode() === 'none' ? 'position' : 'size');
const batchCmd = new BatchCommand(text);
let i = selectedElements.length;
while (i--) {
const elem = selectedElements[i];
// if (getRotationAngle(elem) && !hasMatrixTransform(getTransformList(elem))) { continue; }
const cmd = selectionContext_.getCanvas().recalculateDimensions(elem);
if (cmd) {
batchCmd.addSubCommand(cmd);
}
}
if (!batchCmd.isEmpty()) {
selectionContext_.addCommandToHistory(batchCmd);
selectionContext_.getCanvas().call('changed', selectedElements);
}
};

1085
src/svgcanvas/svg-exec.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

36
src/svgcanvas/svgroot.js Normal file
View File

@@ -0,0 +1,36 @@
/**
* Tools for SVG Root Element.
* @module svgcanvas
* @license MIT
*
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import {NS} from '../common/namespaces.js';
import {text2xml} from '../common/utilities.js';
/**
* @function module:svgcanvas.svgRootElement svgRootElement the svg node and its children.
* @param {Element} svgdoc - window.document
* @param {ArgumentsArray} dimensions - dimensions of width and height
* @returns {svgRootElement}
*/
export const svgRootElement = function (svgdoc, dimensions) {
return svgdoc.importNode(
text2xml(
`<svg id="svgroot" xmlns="${NS.SVG}" xlinkns="${NS.XLINK}" width="${dimensions[0]}"
height="${dimensions[1]}" x="${dimensions[0]}" y="${dimensions[1]}" overflow="visible">
<defs>
<filter id="canvashadow" filterUnits="objectBoundingBox">
<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>
<feOffset in="blur" dx="5" dy="5" result="offsetBlur"/>
<feMerge>
<feMergeNode in="offsetBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
</svg>`
).documentElement,
true
);
};

View File

@@ -0,0 +1,544 @@
/* globals jQuery */
/**
* @module text-actions Tools for Text edit functions
* @license MIT
*
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import jQueryPluginSVG from '../common/jQuery.attr.js';
import {NS} from '../common/namespaces.js';
import {
transformPoint, getMatrix
} from '../common/math.js';
import {
assignAttributes, getElem, getBBox as utilsGetBBox
} from '../common/utilities.js';
import {
supportsGoodTextCharPos
} from '../common/browser.js';
const $ = jQueryPluginSVG(jQuery);
let textActionsContext_ = null;
/**
* @function module:text-actions.init
* @param {module:text-actions.textActionsContext_} textActionsContext
* @returns {void}
*/
export const init = function (textActionsContext) {
textActionsContext_ = textActionsContext;
};
/* eslint-disable jsdoc/require-property */
/**
* Group: Text edit functions
* Functions relating to editing text elements.
* @namespace {PlainObject} textActions
* @memberof module:svgcanvas.SvgCanvas#
*/
export const textActionsMethod = (function () {
/* eslint-enable jsdoc/require-property */
let curtext;
let textinput;
let cursor;
let selblock;
let blinker;
let chardata = [];
let textbb; // , transbb;
let matrix;
let lastX, lastY;
let allowDbl;
/**
*
* @param {Integer} index
* @returns {void}
*/
function setCursor (index) {
const empty = (textinput.value === '');
$(textinput).focus();
if (!arguments.length) {
if (empty) {
index = 0;
} else {
if (textinput.selectionEnd !== textinput.selectionStart) { return; }
index = textinput.selectionEnd;
}
}
const charbb = chardata[index];
if (!empty) {
textinput.setSelectionRange(index, index);
}
cursor = getElem('text_cursor');
if (!cursor) {
cursor = document.createElementNS(NS.SVG, 'line');
assignAttributes(cursor, {
id: 'text_cursor',
stroke: '#333',
'stroke-width': 1
});
cursor = getElem('selectorParentGroup').appendChild(cursor);
}
if (!blinker) {
blinker = setInterval(function () {
const show = (cursor.getAttribute('display') === 'none');
cursor.setAttribute('display', show ? 'inline' : 'none');
}, 600);
}
const startPt = ptToScreen(charbb.x, textbb.y);
const endPt = ptToScreen(charbb.x, (textbb.y + textbb.height));
assignAttributes(cursor, {
x1: startPt.x,
y1: startPt.y,
x2: endPt.x,
y2: endPt.y,
visibility: 'visible',
display: 'inline'
});
if (selblock) { selblock.setAttribute('d', ''); }
}
/**
*
* @param {Integer} start
* @param {Integer} end
* @param {boolean} skipInput
* @returns {void}
*/
function setSelection (start, end, skipInput) {
if (start === end) {
setCursor(end);
return;
}
if (!skipInput) {
textinput.setSelectionRange(start, end);
}
selblock = getElem('text_selectblock');
if (!selblock) {
selblock = document.createElementNS(NS.SVG, 'path');
assignAttributes(selblock, {
id: 'text_selectblock',
fill: 'green',
opacity: 0.5,
style: 'pointer-events:none'
});
getElem('selectorParentGroup').append(selblock);
}
const startbb = chardata[start];
const endbb = chardata[end];
cursor.setAttribute('visibility', 'hidden');
const tl = ptToScreen(startbb.x, textbb.y),
tr = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y),
bl = ptToScreen(startbb.x, textbb.y + textbb.height),
br = ptToScreen(startbb.x + (endbb.x - startbb.x), textbb.y + textbb.height);
const dstr = 'M' + tl.x + ',' + tl.y +
' L' + tr.x + ',' + tr.y +
' ' + br.x + ',' + br.y +
' ' + bl.x + ',' + bl.y + 'z';
assignAttributes(selblock, {
d: dstr,
display: 'inline'
});
}
/**
*
* @param {Float} mouseX
* @param {Float} mouseY
* @returns {Integer}
*/
function getIndexFromPoint (mouseX, mouseY) {
// Position cursor here
const pt = textActionsContext_.getSVGRoot().createSVGPoint();
pt.x = mouseX;
pt.y = mouseY;
// No content, so return 0
if (chardata.length === 1) { return 0; }
// Determine if cursor should be on left or right of character
let charpos = curtext.getCharNumAtPosition(pt);
if (charpos < 0) {
// Out of text range, look at mouse coords
charpos = chardata.length - 2;
if (mouseX <= chardata[0].x) {
charpos = 0;
}
} else if (charpos >= chardata.length - 2) {
charpos = chardata.length - 2;
}
const charbb = chardata[charpos];
const mid = charbb.x + (charbb.width / 2);
if (mouseX > mid) {
charpos++;
}
return charpos;
}
/**
*
* @param {Float} mouseX
* @param {Float} mouseY
* @returns {void}
*/
function setCursorFromPoint (mouseX, mouseY) {
setCursor(getIndexFromPoint(mouseX, mouseY));
}
/**
*
* @param {Float} x
* @param {Float} y
* @param {boolean} apply
* @returns {void}
*/
function setEndSelectionFromPoint (x, y, apply) {
const i1 = textinput.selectionStart;
const i2 = getIndexFromPoint(x, y);
const start = Math.min(i1, i2);
const end = Math.max(i1, i2);
setSelection(start, end, !apply);
}
/**
*
* @param {Float} xIn
* @param {Float} yIn
* @returns {module:math.XYObject}
*/
function screenToPt (xIn, yIn) {
const out = {
x: xIn,
y: yIn
};
const currentZoom = textActionsContext_.getCurrentZoom();
out.x /= currentZoom;
out.y /= currentZoom;
if (matrix) {
const pt = transformPoint(out.x, out.y, matrix.inverse());
out.x = pt.x;
out.y = pt.y;
}
return out;
}
/**
*
* @param {Float} xIn
* @param {Float} yIn
* @returns {module:math.XYObject}
*/
function ptToScreen (xIn, yIn) {
const out = {
x: xIn,
y: yIn
};
if (matrix) {
const pt = transformPoint(out.x, out.y, matrix);
out.x = pt.x;
out.y = pt.y;
}
const currentZoom = textActionsContext_.getCurrentZoom();
out.x *= currentZoom;
out.y *= currentZoom;
return out;
}
/*
// Not currently in use
function hideCursor () {
if (cursor) {
cursor.setAttribute('visibility', 'hidden');
}
}
*/
/**
*
* @param {Event} evt
* @returns {void}
*/
function selectAll (evt) {
setSelection(0, curtext.textContent.length);
$(this).unbind(evt);
}
/**
*
* @param {Event} evt
* @returns {void}
*/
function selectWord (evt) {
if (!allowDbl || !curtext) { return; }
const currentZoom = textActionsContext_.getCurrentZoom();
const ept = transformPoint(evt.pageX, evt.pageY, textActionsContext_.getrootSctm()),
mouseX = ept.x * currentZoom,
mouseY = ept.y * currentZoom;
const pt = screenToPt(mouseX, mouseY);
const index = getIndexFromPoint(pt.x, pt.y);
const str = curtext.textContent;
const first = str.substr(0, index).replace(/[a-z\d]+$/i, '').length;
const m = str.substr(index).match(/^[a-z\d]+/i);
const last = (m ? m[0].length : 0) + index;
setSelection(first, last);
// Set tripleclick
$(evt.target).click(selectAll);
setTimeout(function () {
$(evt.target).unbind('click', selectAll);
}, 300);
}
return /** @lends module:svgcanvas.SvgCanvas#textActions */ {
/**
* @param {Element} target
* @param {Float} x
* @param {Float} y
* @returns {void}
*/
select (target, x, y) {
curtext = target;
textActionsContext_.getCanvas().textActions.toEditMode(x, y);
},
/**
* @param {Element} elem
* @returns {void}
*/
start (elem) {
curtext = elem;
textActionsContext_.getCanvas().textActions.toEditMode();
},
/**
* @param {external:MouseEvent} evt
* @param {Element} mouseTarget
* @param {Float} startX
* @param {Float} startY
* @returns {void}
*/
mouseDown (evt, mouseTarget, startX, startY) {
const pt = screenToPt(startX, startY);
textinput.focus();
setCursorFromPoint(pt.x, pt.y);
lastX = startX;
lastY = startY;
// TODO: Find way to block native selection
},
/**
* @param {Float} mouseX
* @param {Float} mouseY
* @returns {void}
*/
mouseMove (mouseX, mouseY) {
const pt = screenToPt(mouseX, mouseY);
setEndSelectionFromPoint(pt.x, pt.y);
},
/**
* @param {external:MouseEvent} evt
* @param {Float} mouseX
* @param {Float} mouseY
* @returns {void}
*/
mouseUp (evt, mouseX, mouseY) {
const pt = screenToPt(mouseX, mouseY);
setEndSelectionFromPoint(pt.x, pt.y, true);
// TODO: Find a way to make this work: Use transformed BBox instead of evt.target
// if (lastX === mouseX && lastY === mouseY
// && !rectsIntersect(transbb, {x: pt.x, y: pt.y, width: 0, height: 0})) {
// textActionsContext_.getCanvas().textActions.toSelectMode(true);
// }
if (
evt.target !== curtext &&
mouseX < lastX + 2 &&
mouseX > lastX - 2 &&
mouseY < lastY + 2 &&
mouseY > lastY - 2
) {
textActionsContext_.getCanvas().textActions.toSelectMode(true);
}
},
/**
* @function
* @param {Integer} index
* @returns {void}
*/
setCursor,
/**
* @param {Float} x
* @param {Float} y
* @returns {void}
*/
toEditMode (x, y) {
allowDbl = false;
textActionsContext_.setCurrentMode('textedit');
textActionsContext_.getCanvas().selectorManager.requestSelector(curtext).showGrips(false);
// Make selector group accept clicks
/* const selector = */ textActionsContext_.getCanvas().selectorManager.requestSelector(curtext); // Do we need this? Has side effect of setting lock, so keeping for now, but next line wasn't being used
// const sel = selector.selectorRect;
textActionsContext_.getCanvas().textActions.init();
$(curtext).css('cursor', 'text');
// if (supportsEditableText()) {
// curtext.setAttribute('editable', 'simple');
// return;
// }
if (!arguments.length) {
setCursor();
} else {
const pt = screenToPt(x, y);
setCursorFromPoint(pt.x, pt.y);
}
setTimeout(function () {
allowDbl = true;
}, 300);
},
/**
* @param {boolean|Element} selectElem
* @fires module:svgcanvas.SvgCanvas#event:selected
* @returns {void}
*/
toSelectMode (selectElem) {
textActionsContext_.setCurrentMode('select');
clearInterval(blinker);
blinker = null;
if (selblock) { $(selblock).attr('display', 'none'); }
if (cursor) { $(cursor).attr('visibility', 'hidden'); }
$(curtext).css('cursor', 'move');
if (selectElem) {
textActionsContext_.getCanvas().clearSelection();
$(curtext).css('cursor', 'move');
textActionsContext_.call('selected', [curtext]);
textActionsContext_.getCanvas().addToSelection([curtext], true);
}
if (curtext && !curtext.textContent.length) {
// No content, so delete
textActionsContext_.getCanvas().deleteSelectedElements();
}
$(textinput).blur();
curtext = false;
// if (supportsEditableText()) {
// curtext.removeAttribute('editable');
// }
},
/**
* @param {Element} elem
* @returns {void}
*/
setInputElem (elem) {
textinput = elem;
// $(textinput).blur(hideCursor);
},
/**
* @returns {void}
*/
clear () {
if (textActionsContext_.getCurrentMode() === 'textedit') {
textActionsContext_.getCanvas().textActions.toSelectMode();
}
},
/**
* @param {Element} inputElem Not in use
* @returns {void}
*/
init (inputElem) {
if (!curtext) { return; }
let i, end;
// if (supportsEditableText()) {
// curtext.select();
// return;
// }
if (!curtext.parentNode) {
// Result of the ffClone, need to get correct element
const selectedElements = textActionsContext_.getSelectedElements();
curtext = selectedElements[0];
textActionsContext_.getCanvas().selectorManager.requestSelector(curtext).showGrips(false);
}
const str = curtext.textContent;
const len = str.length;
const xform = curtext.getAttribute('transform');
textbb = utilsGetBBox(curtext);
matrix = xform ? getMatrix(curtext) : null;
chardata = [];
chardata.length = len;
textinput.focus();
$(curtext).unbind('dblclick', selectWord).dblclick(selectWord);
if (!len) {
end = {x: textbb.x + (textbb.width / 2), width: 0};
}
for (i = 0; i < len; i++) {
const start = curtext.getStartPositionOfChar(i);
end = curtext.getEndPositionOfChar(i);
if (!supportsGoodTextCharPos()) {
const currentZoom = textActionsContext_.getCurrentZoom();
const offset = textActionsContext_.getCanvas().contentW * currentZoom;
start.x -= offset;
end.x -= offset;
start.x /= currentZoom;
end.x /= currentZoom;
}
// Get a "bbox" equivalent for each character. Uses the
// bbox data of the actual text for y, height purposes
// TODO: Decide if y, width and height are actually necessary
chardata[i] = {
x: start.x,
y: textbb.y, // start.y?
width: end.x - start.x,
height: textbb.height
};
}
// Add a last bbox for cursor at end of text
chardata.push({
x: end.x,
width: 0
});
setSelection(textinput.selectionStart, textinput.selectionEnd, true);
}
};
}());

280
src/svgcanvas/undo.js Normal file
View File

@@ -0,0 +1,280 @@
/**
* Tools for undo.
* @module undo
* @license MIT
* @copyright 2011 Jeff Schiller
*/
import * as draw from './draw.js';
import * as hstry from './history.js';
import {
getRotationAngle, getBBox as utilsGetBBox, isNullish, setHref, getStrokedBBoxDefaultVisible
} from '../common/utilities.js';
import {
isGecko
} from '../common/browser.js';
import {
transformPoint, transformListToTransform
} from '../common/math.js';
import {
getTransformList
} from '../common/svgtransformlist.js';
const {
UndoManager, HistoryEventTypes
} = hstry;
let undoContext_ = null;
/**
* @function module:undo.init
* @param {module:undo.undoContext} undoContext
* @returns {void}
*/
export const init = function (undoContext) {
undoContext_ = undoContext;
};
export const getUndoManager = function () {
return new UndoManager({
/**
* @param {string} eventType One of the HistoryEvent types
* @param {module:history.HistoryCommand} cmd Fulfills the HistoryCommand interface
* @fires module:undo.SvgCanvas#event:changed
* @returns {void}
*/
handleHistoryEvent (eventType, cmd) {
const EventTypes = HistoryEventTypes;
// TODO: handle setBlurOffsets.
if (eventType === EventTypes.BEFORE_UNAPPLY || eventType === EventTypes.BEFORE_APPLY) {
undoContext_.getCanvas().clearSelection();
} else if (eventType === EventTypes.AFTER_APPLY || eventType === EventTypes.AFTER_UNAPPLY) {
const elems = cmd.elements();
undoContext_.getCanvas().pathActions.clear();
undoContext_.call('changed', elems);
const cmdType = cmd.type();
const isApply = (eventType === EventTypes.AFTER_APPLY);
if (cmdType === 'MoveElementCommand') {
const parent = isApply ? cmd.newParent : cmd.oldParent;
if (parent === undoContext_.getSVGContent()) {
draw.identifyLayers();
}
} else if (cmdType === 'InsertElementCommand' || cmdType === 'RemoveElementCommand') {
if (cmd.parent === undoContext_.getSVGContent()) {
draw.identifyLayers();
}
if (cmdType === 'InsertElementCommand') {
if (isApply) {
undoContext_.restoreRefElems(cmd.elem);
}
} else if (!isApply) {
undoContext_.restoreRefElems(cmd.elem);
}
if (cmd.elem && cmd.elem.tagName === 'use') {
undoContext_.getCanvas().setUseData(cmd.elem);
}
} else if (cmdType === 'ChangeElementCommand') {
// if we are changing layer names, re-identify all layers
if (cmd.elem.tagName === 'title' &&
cmd.elem.parentNode.parentNode === undoContext_.getSVGContent()
) {
draw.identifyLayers();
}
const values = isApply ? cmd.newValues : cmd.oldValues;
// If stdDeviation was changed, update the blur.
if (values.stdDeviation) {
undoContext_.getCanvas().setBlurOffsets(cmd.elem.parentNode, values.stdDeviation);
}
// This is resolved in later versions of webkit, perhaps we should
// have a featured detection for correct 'use' behavior?
// ——————————
// Remove & Re-add hack for Webkit (issue 775)
// if (cmd.elem.tagName === 'use' && isWebkit()) {
// const {elem} = cmd;
// if (!elem.getAttribute('x') && !elem.getAttribute('y')) {
// const parent = elem.parentNode;
// const sib = elem.nextSibling;
// elem.remove();
// parent.insertBefore(elem, sib);
// // Ok to replace above with this? `sib.before(elem);`
// }
// }
}
}
}
});
};
/**
* Hack for Firefox bugs where text element features aren't updated or get
* messed up. See issue 136 and issue 137.
* This function clones the element and re-selects it.
* @function module:svgcanvas~ffClone
* @todo Test for this bug on load and add it to "support" object instead of
* browser sniffing
* @param {Element} elem - The (text) DOM element to clone
* @returns {Element} Cloned element
*/
export const ffClone = function (elem) {
if (!isGecko()) { return elem; }
const clone = elem.cloneNode(true);
elem.before(clone);
elem.remove();
undoContext_.getCanvas().selectorManager.releaseSelector(elem);
undoContext_.getCanvas().setSelectedElements(0, clone);
undoContext_.getCanvas().selectorManager.requestSelector(clone).showGrips(true);
return clone;
};
/**
* This function makes the changes to the elements. It does not add the change
* to the history stack.
* @param {string} attr - Attribute name
* @param {string|Float} newValue - String or number with the new attribute value
* @param {Element[]} elems - The DOM elements to apply the change to
* @returns {void}
*/
export const changeSelectedAttributeNoUndoMethod = function (attr, newValue, elems) {
const selectedElements = undoContext_.getSelectedElements();
const currentZoom = undoContext_.getCurrentZoom();
if (undoContext_.getCurrentMode() === 'pathedit') {
// Editing node
undoContext_.getCanvas().pathActions.moveNode(attr, newValue);
}
elems = elems || selectedElements;
let i = elems.length;
const noXYElems = ['g', 'polyline', 'path'];
// const goodGAttrs = ['transform', 'opacity', 'filter'];
while (i--) {
let elem = elems[i];
if (isNullish(elem)) { continue; }
// Set x,y vals on elements that don't have them
if ((attr === 'x' || attr === 'y') && noXYElems.includes(elem.tagName)) {
const bbox = getStrokedBBoxDefaultVisible([elem]);
const diffX = attr === 'x' ? newValue - bbox.x : 0;
const diffY = attr === 'y' ? newValue - bbox.y : 0;
undoContext_.getCanvas().moveSelectedElements(diffX * currentZoom, diffY * currentZoom, true);
continue;
}
// only allow the transform/opacity/filter attribute to change on <g> elements, slightly hacky
// TODO: Missing statement body
// if (elem.tagName === 'g' && goodGAttrs.includes(attr)) {}
let oldval = attr === '#text' ? elem.textContent : elem.getAttribute(attr);
if (isNullish(oldval)) { oldval = ''; }
if (oldval !== String(newValue)) {
if (attr === '#text') {
// const oldW = utilsGetBBox(elem).width;
elem.textContent = newValue;
// FF bug occurs on on rotated elements
if ((/rotate/).test(elem.getAttribute('transform'))) {
elem = ffClone(elem);
}
// Hoped to solve the issue of moving text with text-anchor="start",
// but this doesn't actually fix it. Hopefully on the right track, though. -Fyrd
// const box = getBBox(elem), left = box.x, top = box.y, {width, height} = box,
// dx = width - oldW, dy = 0;
// const angle = getRotationAngle(elem, true);
// if (angle) {
// const r = Math.sqrt(dx * dx + dy * dy);
// const theta = Math.atan2(dy, dx) - angle;
// dx = r * Math.cos(theta);
// dy = r * Math.sin(theta);
//
// elem.setAttribute('x', elem.getAttribute('x') - dx);
// elem.setAttribute('y', elem.getAttribute('y') - dy);
// }
} else if (attr === '#href') {
setHref(elem, newValue);
} else { elem.setAttribute(attr, newValue); }
// Go into "select" mode for text changes
// NOTE: Important that this happens AFTER elem.setAttribute() or else attributes like
// font-size can get reset to their old value, ultimately by svgEditor.updateContextPanel(),
// after calling textActions.toSelectMode() below
if (undoContext_.getCurrentMode() === 'textedit' && attr !== '#text' && elem.textContent.length) {
undoContext_.getCanvas().textActions.toSelectMode(elem);
}
// if (i === 0) {
// selectedBBoxes[0] = utilsGetBBox(elem);
// }
// Use the Firefox ffClone hack for text elements with gradients or
// where other text attributes are changed.
if (isGecko() && elem.nodeName === 'text' && (/rotate/).test(elem.getAttribute('transform'))) {
if (
String(newValue).startsWith('url') || (['font-size', 'font-family', 'x', 'y'].includes(attr) && elem.textContent)
) {
elem = ffClone(elem);
}
}
// Timeout needed for Opera & Firefox
// codedread: it is now possible for this function to be called with elements
// that are not in the selectedElements array, we need to only request a
// selector if the element is in that array
if (selectedElements.includes(elem)) {
// eslint-disable-next-line no-loop-func
setTimeout(function () {
// Due to element replacement, this element may no longer
// be part of the DOM
if (!elem.parentNode) { return; }
undoContext_.getCanvas().selectorManager.requestSelector(elem).resize();
}, 0);
}
// if this element was rotated, and we changed the position of this element
// we need to update the rotational transform attribute
const angle = getRotationAngle(elem);
if (angle !== 0 && attr !== 'transform') {
const tlist = getTransformList(elem);
let n = tlist.numberOfItems;
while (n--) {
const xform = tlist.getItem(n);
if (xform.type === 4) {
// remove old rotate
tlist.removeItem(n);
const box = utilsGetBBox(elem);
const center = transformPoint(
box.x + box.width / 2, box.y + box.height / 2, transformListToTransform(tlist).matrix
);
const cx = center.x,
cy = center.y;
const newrot = undoContext_.getSVGRoot().createSVGTransform();
newrot.setRotate(angle, cx, cy);
tlist.insertItemBefore(newrot, n);
break;
}
}
}
} // if oldValue != newValue
} // for each elem
};
/**
* Change the given/selected element and add the original value to the history stack.
* If you want to change all `selectedElements`, ignore the `elems` argument.
* If you want to change only a subset of `selectedElements`, then send the
* subset to this function in the `elems` argument.
* @function module:svgcanvas.SvgCanvas#changeSelectedAttribute
* @param {string} attr - String with the attribute name
* @param {string|Float} val - String or number with the new attribute value
* @param {Element[]} elems - The DOM elements to apply the change to
* @returns {void}
*/
export const changeSelectedAttributeMethod = function (attr, val, elems) {
const selectedElements = undoContext_.getSelectedElements();
elems = elems || selectedElements;
undoContext_.getCanvas().undoMgr.beginUndoableChange(attr, elems);
// const i = elems.length;
changeSelectedAttributeNoUndoMethod(attr, val, elems);
const batchCmd = undoContext_.getCanvas().undoMgr.finishUndoableChange();
if (!batchCmd.isEmpty()) {
// undoContext_.addCommandToHistory(batchCmd);
undoContext_.getCanvas().undoMgr.addCommandToHistory(batchCmd);
}
};