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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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
161
src/svgcanvas/blur-event.js
Normal 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
45
src/svgcanvas/clear.js
Normal 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);
|
||||
};
|
||||
886
src/svgcanvas/elem-get-set.js
Normal file
886
src/svgcanvas/elem-get-set.js
Normal 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
1388
src/svgcanvas/event.js
Normal file
File diff suppressed because it is too large
Load Diff
110
src/svgcanvas/json.js
Normal file
110
src/svgcanvas/json.js
Normal 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
132
src/svgcanvas/paste-elem.js
Normal 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);
|
||||
};
|
||||
1256
src/svgcanvas/path-actions.js
Normal file
1256
src/svgcanvas/path-actions.js
Normal file
File diff suppressed because it is too large
Load Diff
1062
src/svgcanvas/path-method.js
Normal file
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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
989
src/svgcanvas/selected-elem.js
Normal file
989
src/svgcanvas/selected-elem.js
Normal 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
444
src/svgcanvas/selection.js
Normal 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
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
36
src/svgcanvas/svgroot.js
Normal 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
|
||||
);
|
||||
};
|
||||
544
src/svgcanvas/text-actions.js
Normal file
544
src/svgcanvas/text-actions.js
Normal 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
280
src/svgcanvas/undo.js
Normal 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user