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:
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);
|
||||
};
|
||||
Reference in New Issue
Block a user