update master to V7

This commit is contained in:
JFH
2021-05-09 19:29:45 +02:00
parent 41fc05672d
commit 593c415664
1000 changed files with 47537 additions and 54304 deletions

View File

@@ -44,7 +44,7 @@ export const setBlurNoUndo = function (val) {
blurContext_.changeSelectedAttributeNoUndoMethod('filter', 'url(#' + elem.id + '_blur)');
}
if (blurContext_.isWebkit()) {
// console.log('e', elem); // eslint-disable-line no-console
// console.log('e', elem);
elem.removeAttribute('filter');
elem.setAttribute('filter', 'url(#' + elem.id + '_blur)');
}

View File

@@ -1,15 +1,11 @@
/* 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;
/**
@@ -24,20 +20,23 @@ export const init = function (clearContext) {
export const clearSvgContentElementInit = function () {
const curConfig = clearContext_.getCurConfig();
const {dimensions} = curConfig;
$(clearContext_.getSVGContent()).empty();
const el = clearContext_.getSVGContent();
// empty()
while(el.firstChild)
el.removeChild(el.firstChild);
// 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());
const pel = clearContext_.getSVGRoot();
el.setAttribute('id', 'svgcontent');
el.setAttribute('width', dimensions[0]);
el.setAttribute('height', dimensions[1]);
el.setAttribute('x', dimensions[0]);
el.setAttribute('y', dimensions[1]);
el.setAttribute('overflow', curConfig.show_outside_canvas ? 'visible' : 'hidden');
el.setAttribute('xmlns', NS.SVG);
el.setAttribute('xmlns:se', NS.SE);
el.setAttribute('xmlns:xlink', NS.XLINK);
pel.appendChild(el);
// 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');

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* Manipulating coordinates.
* @module coords
@@ -7,13 +6,11 @@
import {
snapToGrid, assignAttributes, getBBox, getRefElem, findDefs
} from '../common/utilities.js';
} from './utilities.js';
import {
transformPoint, transformListToTransform, matrixMultiply, transformBox
} from '../common/math.js';
import {getTransformList} from '../common/svgtransformlist.js';
const $ = jQuery;
} from './math.js';
import { getTransformList } from './svgtransformlist.js';
// this is how we map paths to our preferred relative segment types
const pathMap = [
@@ -71,34 +68,27 @@ export const remapElement = function (selected, changes, m) {
for (let i = 0; i < 2; i++) {
const type = i === 0 ? 'fill' : 'stroke';
const attrVal = selected.getAttribute(type);
if (attrVal && attrVal.startsWith('url(')) {
if (m.a < 0 || m.d < 0) {
const grad = getRefElem(attrVal);
const newgrad = grad.cloneNode(true);
if (m.a < 0) {
// flip x
const x1 = newgrad.getAttribute('x1');
const x2 = newgrad.getAttribute('x2');
newgrad.setAttribute('x1', -(x1 - 1));
newgrad.setAttribute('x2', -(x2 - 1));
}
if (m.d < 0) {
// flip y
const y1 = newgrad.getAttribute('y1');
const y2 = newgrad.getAttribute('y2');
newgrad.setAttribute('y1', -(y1 - 1));
newgrad.setAttribute('y2', -(y2 - 1));
}
newgrad.id = editorContext_.getDrawing().getNextId();
findDefs().append(newgrad);
selected.setAttribute(type, 'url(#' + newgrad.id + ')');
if (attrVal && attrVal.startsWith('url(') && (m.a < 0 || m.d < 0)) {
const grad = getRefElem(attrVal);
const newgrad = grad.cloneNode(true);
if (m.a < 0) {
// flip x
const x1 = newgrad.getAttribute('x1');
const x2 = newgrad.getAttribute('x2');
newgrad.setAttribute('x1', -(x1 - 1));
newgrad.setAttribute('x2', -(x2 - 1));
}
// Not really working :(
// if (selected.tagName === 'path') {
// reorientGrads(selected, m);
// }
if (m.d < 0) {
// flip y
const y1 = newgrad.getAttribute('y1');
const y2 = newgrad.getAttribute('y2');
newgrad.setAttribute('y1', -(y1 - 1));
newgrad.setAttribute('y2', -(y2 - 1));
}
newgrad.id = editorContext_.getDrawing().getNextId();
findDefs().append(newgrad);
selected.setAttribute(type, 'url(#' + newgrad.id + ')');
}
}
@@ -125,191 +115,192 @@ export const remapElement = function (selected, changes, m) {
// now we have a set of changes and an applied reduced transform list
// we apply the changes directly to the DOM
switch (elName) {
case 'foreignObject':
case 'rect':
case 'image': {
// Allow images to be inverted (give them matrix when flipped)
if (elName === 'image' && (m.a < 0 || m.d < 0)) {
// Convert to matrix
const chlist = getTransformList(selected);
const mt = editorContext_.getSVGRoot().createSVGTransform();
mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m));
chlist.clear();
chlist.appendItem(mt);
} else {
const pt1 = remap(changes.x, changes.y);
changes.width = scalew(changes.width);
changes.height = scaleh(changes.height);
changes.x = pt1.x + Math.min(0, changes.width);
changes.y = pt1.y + Math.min(0, changes.height);
changes.width = Math.abs(changes.width);
changes.height = Math.abs(changes.height);
}
finishUp();
break;
} case 'ellipse': {
const c = remap(changes.cx, changes.cy);
changes.cx = c.x;
changes.cy = c.y;
changes.rx = scalew(changes.rx);
changes.ry = scaleh(changes.ry);
changes.rx = Math.abs(changes.rx);
changes.ry = Math.abs(changes.ry);
finishUp();
break;
} case 'circle': {
const c = remap(changes.cx, changes.cy);
changes.cx = c.x;
changes.cy = c.y;
// take the minimum of the new selected box's dimensions for the new circle radius
const tbox = transformBox(box.x, box.y, box.width, box.height, m);
const w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y;
changes.r = Math.min(w / 2, h / 2);
if (changes.r) { changes.r = Math.abs(changes.r); }
finishUp();
break;
} case 'line': {
const pt1 = remap(changes.x1, changes.y1);
const pt2 = remap(changes.x2, changes.y2);
changes.x1 = pt1.x;
changes.y1 = pt1.y;
changes.x2 = pt2.x;
changes.y2 = pt2.y;
} // Fallthrough
case 'text':
case 'tspan':
case 'use': {
finishUp();
break;
} case 'g': {
const gsvg = $(selected).data('gsvg');
if (gsvg) {
assignAttributes(gsvg, changes, 1000, true);
}
break;
} case 'polyline':
case 'polygon': {
const len = changes.points.length;
for (let i = 0; i < len; ++i) {
const pt = changes.points[i];
const {x, y} = remap(pt.x, pt.y);
changes.points[i].x = x;
changes.points[i].y = y;
}
// const len = changes.points.length;
let pstr = '';
for (let i = 0; i < len; ++i) {
const pt = changes.points[i];
pstr += pt.x + ',' + pt.y + ' ';
}
selected.setAttribute('points', pstr);
break;
} case 'path': {
const segList = selected.pathSegList;
let len = segList.numberOfItems;
changes.d = [];
for (let i = 0; i < len; ++i) {
const seg = segList.getItem(i);
changes.d[i] = {
type: seg.pathSegType,
x: seg.x,
y: seg.y,
x1: seg.x1,
y1: seg.y1,
x2: seg.x2,
y2: seg.y2,
r1: seg.r1,
r2: seg.r2,
angle: seg.angle,
largeArcFlag: seg.largeArcFlag,
sweepFlag: seg.sweepFlag
};
}
len = changes.d.length;
const firstseg = changes.d[0],
currentpt = remap(firstseg.x, firstseg.y);
changes.d[0].x = currentpt.x;
changes.d[0].y = currentpt.y;
for (let i = 1; i < len; ++i) {
const seg = changes.d[i];
const {type} = seg;
// if absolute or first segment, we want to remap x, y, x1, y1, x2, y2
// if relative, we want to scalew, scaleh
if (type % 2 === 0) { // absolute
const thisx = (seg.x !== undefined) ? seg.x : currentpt.x, // for V commands
thisy = (seg.y !== undefined) ? seg.y : currentpt.y; // for H commands
const pt = remap(thisx, thisy);
const pt1 = remap(seg.x1, seg.y1);
const pt2 = remap(seg.x2, seg.y2);
seg.x = pt.x;
seg.y = pt.y;
seg.x1 = pt1.x;
seg.y1 = pt1.y;
seg.x2 = pt2.x;
seg.y2 = pt2.y;
seg.r1 = scalew(seg.r1);
seg.r2 = scaleh(seg.r2);
} else { // relative
seg.x = scalew(seg.x);
seg.y = scaleh(seg.y);
seg.x1 = scalew(seg.x1);
seg.y1 = scaleh(seg.y1);
seg.x2 = scalew(seg.x2);
seg.y2 = scaleh(seg.y2);
seg.r1 = scalew(seg.r1);
seg.r2 = scaleh(seg.r2);
case 'foreignObject':
case 'rect':
case 'image': {
// Allow images to be inverted (give them matrix when flipped)
if (elName === 'image' && (m.a < 0 || m.d < 0)) {
// Convert to matrix
const chlist = getTransformList(selected);
const mt = editorContext_.getSVGRoot().createSVGTransform();
mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m));
chlist.clear();
chlist.appendItem(mt);
} else {
const pt1 = remap(changes.x, changes.y);
changes.width = scalew(changes.width);
changes.height = scaleh(changes.height);
changes.x = pt1.x + Math.min(0, changes.width);
changes.y = pt1.y + Math.min(0, changes.height);
changes.width = Math.abs(changes.width);
changes.height = Math.abs(changes.height);
}
} // for each segment
finishUp();
break;
} case 'ellipse': {
const c = remap(changes.cx, changes.cy);
changes.cx = c.x;
changes.cy = c.y;
changes.rx = scalew(changes.rx);
changes.ry = scaleh(changes.ry);
changes.rx = Math.abs(changes.rx);
changes.ry = Math.abs(changes.ry);
finishUp();
break;
} case 'circle': {
const c = remap(changes.cx, changes.cy);
changes.cx = c.x;
changes.cy = c.y;
// take the minimum of the new selected box's dimensions for the new circle radius
const tbox = transformBox(box.x, box.y, box.width, box.height, m);
const w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y;
changes.r = Math.min(w / 2, h / 2);
let dstr = '';
len = changes.d.length;
for (let i = 0; i < len; ++i) {
const seg = changes.d[i];
const {type} = seg;
dstr += pathMap[type];
switch (type) {
case 13: // relative horizontal line (h)
case 12: // absolute horizontal line (H)
dstr += seg.x + ' ';
break;
case 15: // relative vertical line (v)
case 14: // absolute vertical line (V)
dstr += seg.y + ' ';
break;
case 3: // relative move (m)
case 5: // relative line (l)
case 19: // relative smooth quad (t)
case 2: // absolute move (M)
case 4: // absolute line (L)
case 18: // absolute smooth quad (T)
dstr += seg.x + ',' + seg.y + ' ';
break;
case 7: // relative cubic (c)
case 6: // absolute cubic (C)
dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x2 + ',' + seg.y2 + ' ' +
seg.x + ',' + seg.y + ' ';
break;
case 9: // relative quad (q)
case 8: // absolute quad (Q)
dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x + ',' + seg.y + ' ';
break;
case 11: // relative elliptical arc (a)
case 10: // absolute elliptical arc (A)
dstr += seg.r1 + ',' + seg.r2 + ' ' + seg.angle + ' ' + Number(seg.largeArcFlag) +
' ' + Number(seg.sweepFlag) + ' ' + seg.x + ',' + seg.y + ' ';
break;
case 17: // relative smooth cubic (s)
case 16: // absolute smooth cubic (S)
dstr += seg.x2 + ',' + seg.y2 + ' ' + seg.x + ',' + seg.y + ' ';
break;
if (changes.r) { changes.r = Math.abs(changes.r); }
finishUp();
break;
} case 'line': {
const pt1 = remap(changes.x1, changes.y1);
const pt2 = remap(changes.x2, changes.y2);
changes.x1 = pt1.x;
changes.y1 = pt1.y;
changes.x2 = pt2.x;
changes.y2 = pt2.y;
} // Fallthrough
case 'text':
case 'tspan':
case 'use': {
finishUp();
break;
} case 'g': {
const dataStorage = editorContext_.getDataStorage();
const gsvg = dataStorage.get(selected, 'gsvg');
if (gsvg) {
assignAttributes(gsvg, changes, 1000, true);
}
break;
} case 'polyline':
case 'polygon': {
const len = changes.points.length;
for (let i = 0; i < len; ++i) {
const pt = changes.points[i];
const { x, y } = remap(pt.x, pt.y);
changes.points[i].x = x;
changes.points[i].y = y;
}
}
selected.setAttribute('d', dstr);
break;
}
// const len = changes.points.length;
let pstr = '';
for (let i = 0; i < len; ++i) {
const pt = changes.points[i];
pstr += pt.x + ',' + pt.y + ' ';
}
selected.setAttribute('points', pstr);
break;
} case 'path': {
const segList = selected.pathSegList;
let len = segList.numberOfItems;
changes.d = [];
for (let i = 0; i < len; ++i) {
const seg = segList.getItem(i);
changes.d[i] = {
type: seg.pathSegType,
x: seg.x,
y: seg.y,
x1: seg.x1,
y1: seg.y1,
x2: seg.x2,
y2: seg.y2,
r1: seg.r1,
r2: seg.r2,
angle: seg.angle,
largeArcFlag: seg.largeArcFlag,
sweepFlag: seg.sweepFlag
};
}
len = changes.d.length;
const firstseg = changes.d[0],
currentpt = remap(firstseg.x, firstseg.y);
changes.d[0].x = currentpt.x;
changes.d[0].y = currentpt.y;
for (let i = 1; i < len; ++i) {
const seg = changes.d[i];
const { type } = seg;
// if absolute or first segment, we want to remap x, y, x1, y1, x2, y2
// if relative, we want to scalew, scaleh
if (type % 2 === 0) { // absolute
const thisx = (seg.x !== undefined) ? seg.x : currentpt.x, // for V commands
thisy = (seg.y !== undefined) ? seg.y : currentpt.y; // for H commands
const pt = remap(thisx, thisy);
const pt1 = remap(seg.x1, seg.y1);
const pt2 = remap(seg.x2, seg.y2);
seg.x = pt.x;
seg.y = pt.y;
seg.x1 = pt1.x;
seg.y1 = pt1.y;
seg.x2 = pt2.x;
seg.y2 = pt2.y;
seg.r1 = scalew(seg.r1);
seg.r2 = scaleh(seg.r2);
} else { // relative
seg.x = scalew(seg.x);
seg.y = scaleh(seg.y);
seg.x1 = scalew(seg.x1);
seg.y1 = scaleh(seg.y1);
seg.x2 = scalew(seg.x2);
seg.y2 = scaleh(seg.y2);
seg.r1 = scalew(seg.r1);
seg.r2 = scaleh(seg.r2);
}
} // for each segment
let dstr = '';
len = changes.d.length;
for (let i = 0; i < len; ++i) {
const seg = changes.d[i];
const { type } = seg;
dstr += pathMap[type];
switch (type) {
case 13: // relative horizontal line (h)
case 12: // absolute horizontal line (H)
dstr += seg.x + ' ';
break;
case 15: // relative vertical line (v)
case 14: // absolute vertical line (V)
dstr += seg.y + ' ';
break;
case 3: // relative move (m)
case 5: // relative line (l)
case 19: // relative smooth quad (t)
case 2: // absolute move (M)
case 4: // absolute line (L)
case 18: // absolute smooth quad (T)
dstr += seg.x + ',' + seg.y + ' ';
break;
case 7: // relative cubic (c)
case 6: // absolute cubic (C)
dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x2 + ',' + seg.y2 + ' ' +
seg.x + ',' + seg.y + ' ';
break;
case 9: // relative quad (q)
case 8: // absolute quad (Q)
dstr += seg.x1 + ',' + seg.y1 + ' ' + seg.x + ',' + seg.y + ' ';
break;
case 11: // relative elliptical arc (a)
case 10: // absolute elliptical arc (A)
dstr += seg.r1 + ',' + seg.r2 + ' ' + seg.angle + ' ' + Number(seg.largeArcFlag) +
' ' + Number(seg.sweepFlag) + ' ' + seg.x + ',' + seg.y + ' ';
break;
case 17: // relative smooth cubic (s)
case 16: // absolute smooth cubic (S)
dstr += seg.x2 + ',' + seg.y2 + ' ' + seg.x + ',' + seg.y + ' ';
break;
}
}
selected.setAttribute('d', dstr);
break;
}
}
};

View File

@@ -1,12 +1,6 @@
/* globals jQuery */
import jQueryPluginSVG from '../common/jQuery.attr.js'; // Needed for SVG attribute setting and array form with `attr`
import {isWebkit} from '../common/browser.js';
import {convertPath} from './path.js';
import {preventClickDefault} from '../common/utilities.js';
// Constants
const $ = jQueryPluginSVG(jQuery);
import {preventClickDefault} from './utilities.js';
/**
* Create a clone of an element, updating its ID and its children's IDs when needed.
@@ -18,10 +12,8 @@ const $ = jQueryPluginSVG(jQuery);
export const copyElem = function (el, getNextId) {
// manually create a copy of the element
const newEl = document.createElementNS(el.namespaceURI, el.nodeName);
$.each(el.attributes, function (i, attr) {
if (attr.localName !== '-moz-math-font-style') {
Object.values(el.attributes).forEach((attr) => {
newEl.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value);
}
});
// set the copied element's new id
newEl.removeAttribute('id');
@@ -35,7 +27,7 @@ export const copyElem = function (el, getNextId) {
}
// now create copies of all children
$.each(el.childNodes, function (i, child) {
el.childNodes.forEach(function(child){
switch (child.nodeType) {
case 1: // element node
newEl.append(copyElem(child, getNextId));
@@ -48,11 +40,12 @@ export const copyElem = function (el, getNextId) {
}
});
if ($(el).data('gsvg')) {
$(newEl).data('gsvg', newEl.firstChild);
} else if ($(el).data('symbol')) {
const ref = $(el).data('symbol');
$(newEl).data('ref', ref).data('symbol', ref);
if (el.dataset.gsvg) {
newEl.dataset.gsvg = newEl.firstChild
} else if (el.dataset.symbol) {
const ref = el.dataset.symbol;
newEl.dataset.ref = ref;
newEl.dataset.symbol = ref;
} else if (newEl.tagName === 'image') {
preventClickDefault(newEl);
}

View File

@@ -1,178 +0,0 @@
/**
* @module jQueryPluginDBox
*/
/**
* @param {external:jQuery} $
* @param {PlainObject} [strings]
* @param {PlainObject} [strings.ok]
* @param {PlainObject} [strings.cancel]
* @returns {external:jQuery}
*/
export default function jQueryPluginDBox ($, {
ok: okString = 'Ok',
cancel: cancelString = 'Cancel'
} = {}) {
// This sets up alternative dialog boxes. They mostly work the same way as
// their UI counterparts, expect instead of returning the result, a callback
// needs to be included that returns the result as its first parameter.
// In the future we may want to add additional types of dialog boxes, since
// they should be easy to handle this way.
$('#dialog_container').draggable({
cancel: '#dialog_content, #dialog_buttons *',
containment: 'window'
}).css('position', 'absolute');
const box = $('#dialog_box'),
btnHolder = $('#dialog_buttons'),
dialogContent = $('#dialog_content');
/**
* @typedef {PlainObject} module:jQueryPluginDBox.PromiseResultObject
* @property {string|true} response
* @property {boolean} checked
*/
/**
* Resolves to `false` (if cancelled), for prompts and selects
* without checkboxes, it resolves to the value of the form control. For other
* types without checkboxes, it resolves to `true`. For checkboxes, it resolves
* to an object with the `response` key containing the same value as the previous
* mentioned (string or `true`) and a `checked` (boolean) property.
* @typedef {Promise<boolean|string|module:jQueryPluginDBox.PromiseResultObject>} module:jQueryPluginDBox.ResultPromise
*/
/**
* @typedef {PlainObject} module:jQueryPluginDBox.SelectOption
* @property {string} text
* @property {string} value
*/
/**
* @typedef {PlainObject} module:jQueryPluginDBox.CheckboxInfo
* @property {string} label Label for the checkbox
* @property {string} value Value of the checkbox
* @property {string} tooltip Tooltip on the checkbox label
* @property {boolean} checked Whether the checkbox is checked by default
*/
/**
* Triggered upon a change of value for the select pull-down.
* @callback module:jQueryPluginDBox.SelectChangeListener
* @returns {void}
*/
/**
* Creates a dialog of the specified type with a given message
* and any defaults and type-specific metadata. Returns a `Promise`
* which resolves differently depending on whether the dialog
* was cancelled or okayed (with the response and any checked state).
* @param {"alert"|"prompt"|"select"|"process"} type
* @param {string} msg
* @param {string} [defaultVal]
* @param {module:jQueryPluginDBox.SelectOption[]} [opts]
* @param {module:jQueryPluginDBox.SelectChangeListener} [changeListener]
* @param {module:jQueryPluginDBox.CheckboxInfo} [checkbox]
* @returns {jQueryPluginDBox.ResultPromise}
*/
function dbox (type, msg, defaultVal, opts, changeListener, checkbox) {
dialogContent.html('<p>' + msg.replace(/\n/g, '</p><p>') + '</p>')
.toggleClass('prompt', (type === 'prompt'));
btnHolder.empty();
const ok = $('<input type="button" data-ok="" value="' + okString + '">').appendTo(btnHolder);
return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new
if (type !== 'alert') {
$('<input type="button" value="' + cancelString + '">')
.appendTo(btnHolder)
.click(function () {
box.hide();
resolve(false);
});
}
let ctrl, chkbx;
if (type === 'prompt') {
ctrl = $('<input type="text">').prependTo(btnHolder);
ctrl.val(defaultVal || '');
ctrl.bind('keydown', 'return', function () { ok.click(); });
} else if (type === 'select') {
const div = $('<div style="text-align:center;">');
ctrl = $(`<select aria-label="${msg}">`).appendTo(div);
if (checkbox) {
const label = $('<label>').text(checkbox.label);
chkbx = $('<input type="checkbox">').appendTo(label);
chkbx.val(checkbox.value);
if (checkbox.tooltip) {
label.attr('title', checkbox.tooltip);
}
chkbx.prop('checked', Boolean(checkbox.checked));
div.append($('<div>').append(label));
}
$.each(opts || [], function (opt, val) {
if (typeof val === 'object') {
ctrl.append($('<option>').val(val.value).html(val.text));
} else {
ctrl.append($('<option>').html(val));
}
});
dialogContent.append(div);
if (defaultVal) {
ctrl.val(defaultVal);
}
if (changeListener) {
ctrl.bind('change', 'return', changeListener);
}
ctrl.bind('keydown', 'return', function () { ok.click(); });
} else if (type === 'process') {
ok.hide();
}
box.show();
ok.click(function () {
box.hide();
const response = (type === 'prompt' || type === 'select') ? ctrl.val() : true;
if (chkbx) {
resolve({response, checked: chkbx.prop('checked')});
return;
}
resolve(response);
}).focus();
if (type === 'prompt' || type === 'select') {
ctrl.focus();
}
});
}
/**
* @param {string} msg Message to alert
* @returns {jQueryPluginDBox.ResultPromise}
*/
$.alert = function (msg) {
return dbox('alert', msg);
};
/**
* @param {string} msg Message for which to ask confirmation
* @returns {jQueryPluginDBox.ResultPromise}
*/
$.confirm = function (msg) {
return dbox('confirm', msg);
};
/**
* @param {string} msg Message to indicate upon cancelable indicator
* @returns {jQueryPluginDBox.ResultPromise}
*/
$.process_cancel = function (msg) {
return dbox('process', msg);
};
/**
* @param {string} msg Message to accompany the prompt
* @param {string} [defaultText=""] The default text to show for the prompt
* @returns {jQueryPluginDBox.ResultPromise}
*/
$.prompt = function (msg, defaultText = '') {
return dbox('prompt', msg, defaultText);
};
$.select = function (msg, opts, changeListener, txt, checkbox) {
return dbox('select', msg, txt, opts, changeListener, checkbox);
};
return $;
}

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* Tools for drawing.
* @module draw
@@ -6,22 +5,21 @@
* @copyright 2011 Jeff Schiller
*/
import Layer from '../common/layer.js';
import Layer from './layer.js';
import HistoryRecordingService from './historyrecording.js';
import {NS} from '../common/namespaces.js';
import {isOpera} from '../common/browser.js';
import {
toXml, getElem
} from '../common/utilities.js';
} from './utilities.js';
import {
copyElem as utilCopyElem
} from './copy-elem.js';
import {
BatchCommand, RemoveElementCommand, MoveElementCommand, ChangeElementCommand
} from './history.js';
const $ = jQuery;
import {getParentsUntil} from '../editor/components/jgraduate/Util.js';
const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(',');
@@ -49,10 +47,10 @@ function historyRecordingService (hrService) {
* @returns {string} The layer name or empty string.
*/
function findLayerNameInGroup (group) {
return $('title', group).text() ||
return group.querySelector('title').textContent ||
(isOpera() && group.querySelectorAll
// Hack for Opera 10.60
? $(group.querySelectorAll('title')).text()
? group.querySelector('title').textContent
: '');
}
@@ -158,7 +156,7 @@ export class Drawing {
return this.svgElem_.querySelector('#' + id);
}
// jQuery lookup: twice as slow as xpath in FF
return $(this.svgElem_).find('[id=' + id + ']')[0];
return this.svgElem_.querySelector('[id=' + id + ']');
}
/**
@@ -340,29 +338,6 @@ export class Drawing {
return finalName;
}
/**
* Returns all objects of the currently selected layer. If an error occurs, an empty array is returned.
* @returns {SVGGElement[] | any[]} The objects of the currently active layer (or an empty array if no objects found).
*/
getCurrentLayerChildren () {
return this.current_layer
? [...this.current_layer.getChildren()].filter((object) => { return object.tagName !== 'title'; })
: [];
}
/**
* Returns the object at the current layer with the given 'objectId'. If none is found 'null' is returned.
* @param {string} objectId The id of the object
* @returns {?SVGGElement} The found object or 'null' if none is found.
*/
getCurrentLayerChild (objectId) {
const foundElements = this.getCurrentLayerChildren()
.filter((obj) => { return obj.id === objectId; });
if (!foundElements) { return null; }
return foundElements[0];
}
/**
* Set the current layer's position.
* @param {Integer} newpos - The zero-based index of the new position of the layer. Range should be 0 to layers-1
@@ -414,7 +389,7 @@ export class Drawing {
*/
mergeLayer (hrService) {
const currentGroup = this.current_layer.getGroup();
const prevGroup = $(currentGroup).prev()[0];
const prevGroup = currentGroup.previousElementSibling;
if (!prevGroup) { return; }
hrService.startBatchCommand('Merge Layer');
@@ -653,38 +628,6 @@ export class Drawing {
return layer.getGroup();
}
/**
* Sets the visibility of the object. If the object id is not valid, this
* function returns `null`, otherwise it returns the `SVGElement` representing
* the object. This is an undo-able action.
* @param {string} objectId - The id of the object to change the visibility
* @param {boolean} bVisible - Whether the object should be visible
* @returns {?SVGGElement} The SVGGElement representing the object if the
* `objectId` was valid, otherwise `null`.
*/
setLayerChildrenVisible (objectId, bVisible) {
if (typeof bVisible !== 'boolean') {
return null;
}
const element = this.getCurrentLayerChild(objectId);
const expected = bVisible ? 'inline' : 'none';
const oldDisplay = element.getAttribute('display');
if (oldDisplay !== expected) {
element.setAttribute('display', expected);
}
return element;
}
/**
* Returns whether the object with the given id is visible or not.
* @param {string} objectId - id of the object on which to get the visibility.
* @returns {false|boolean} The visibility of the object, or `false` if the objects id was invalid.
*/
isLayerChildrenVisible (objectId) {
const element = this.getCurrentLayerChild(objectId);
return element.getAttribute('display') !== 'none';
}
/**
* Returns the opacity of the given layer. If the input name is not a layer, `null` is returned.
* @param {string} layerName - name of the layer on which to get the opacity
@@ -765,7 +708,6 @@ export const randomizeIds = function (enableRandomization, currentDrawing) {
/**
* @interface module:draw.DrawCanvasInit
* @property {module:path.pathActions} pathActions
* @property {external:jQuery.data} elData
* @property {module:history.UndoManager} undoMgr
*/
/**
@@ -1044,10 +986,11 @@ export const mergeAllLayers = function (hrService) {
*/
export const leaveContext = function () {
const len = disabledElems.length;
const dataStorage = canvas_.getDataStorage();
if (len) {
for (let i = 0; i < len; i++) {
const elem = disabledElems[i];
const orig = canvas_.elData(elem, 'orig_opac');
const orig = dataStorage.get(elem, 'orig_opac');
if (orig !== 1) {
elem.setAttribute('opacity', orig);
} else {
@@ -1070,6 +1013,7 @@ export const leaveContext = function () {
* @returns {void}
*/
export const setContext = function (elem) {
const dataStorage = canvas_.getDataStorage();
leaveContext();
if (typeof elem === 'string') {
elem = getElem(elem);
@@ -1079,15 +1023,25 @@ export const setContext = function (elem) {
canvas_.setCurrentGroup(elem);
// Disable other elements
$(elem).parentsUntil('#svgcontent').andSelf().siblings().each(function () {
const opac = this.getAttribute('opacity') || 1;
// Store the original's opacity
canvas_.elData(this, 'orig_opac', opac);
this.setAttribute('opacity', opac * 0.33);
this.setAttribute('style', 'pointer-events: none');
disabledElems.push(this);
const parentsUntil = getParentsUntil(elem, '#svgcontent');
let siblings = [];
parentsUntil.forEach(function (parent) {
const elements = Array.prototype.filter.call(parent.parentNode.children, function(child){
return child !== parent;
});
elements.forEach(function (element) {
siblings.push(element);
});
});
siblings.forEach(function (curthis) {
const opac = curthis.getAttribute('opacity') || 1;
// Store the original's opacity
dataStorage.put(curthis, 'orig_opac', opac);
curthis.setAttribute('opacity', opac * 0.33);
curthis.setAttribute('style', 'pointer-events: none');
disabledElems.push(curthis);
});
canvas_.clearSelection();
canvas_.call('contextset', canvas_.getCurrentGroup());
};

View File

@@ -1,20 +1,23 @@
/* globals jQuery */
/**
* @module elem-get-set get and set methods.
* @license MIT
* @copyright 2011 Jeff Schiller
*/
/* globals jQuery */
import { jGraduate } from '../editor/components/jgraduate/jQuery.jGraduate.js';
import * as hstry from './history.js';
import jQueryPluginSVG from '../common/jQuery.attr.js';
import {NS} from '../common/namespaces.js';
import jQueryPluginSVG from './jQuery.attr.js';
import { NS } from '../common/namespaces.js';
import {
getVisibleElements, getStrokedBBoxDefaultVisible, findDefs,
walkTree, isNullish, getHref, setHref, getElem
} from '../common/utilities.js';
} from './utilities.js';
import {
convertToNum
} from '../common/units.js';
import { getParents } from '../editor/components/jgraduate/Util.js';
const $ = jQueryPluginSVG(jQuery);
@@ -58,9 +61,14 @@ export const getResolutionMethod = function () {
*/
export const getTitleMethod = function (elem) {
const selectedElements = elemContext_.getSelectedElements();
const dataStorage = elemContext_.getDataStorage();
elem = elem || selectedElements[0];
if (!elem) { return undefined; }
elem = $(elem).data('gsvg') || $(elem).data('symbol') || elem;
if (dataStorage.has(elem, 'gsvg')) {
elem = dataStorage.get(elem, 'gsvg');
} else if (dataStorage.has(elem, 'symbol')) {
elem = dataStorage.get(elem, 'symbol');
}
const childs = elem.childNodes;
for (const child of childs) {
if (child.nodeName === 'title') {
@@ -79,10 +87,13 @@ export const getTitleMethod = function (elem) {
*/
export const setGroupTitleMethod = function (val) {
const selectedElements = elemContext_.getSelectedElements();
const dataStorage = elemContext_.getDataStorage();
let elem = selectedElements[0];
elem = $(elem).data('gsvg') || elem;
if (dataStorage.has(elem, 'gsvg')) {
elem = dataStorage.get(elem, 'gsvg');
}
const ts = $(elem).children('title');
const ts = elem.querySelectorAll('title');
const batchCmd = new BatchCommand('Set Label');
@@ -95,13 +106,13 @@ export const setGroupTitleMethod = function (val) {
} else if (ts.length) {
// Change title contents
title = ts[0];
batchCmd.addSubCommand(new ChangeElementCommand(title, {'#text': title.textContent}));
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);
elem.insertBefore(title, elem.firstChild);
batchCmd.addSubCommand(new InsertElementCommand(title));
}
@@ -140,7 +151,7 @@ export const setDocumentTitleMethod = function (newTitle) {
// No title given, so element is not necessary
docTitle.remove();
}
batchCmd.addSubCommand(new ChangeElementCommand(docTitle, {'#text': oldTitle}));
batchCmd.addSubCommand(new ChangeElementCommand(docTitle, { '#text': oldTitle }));
elemContext_.addCommandToHistory(batchCmd);
};
@@ -157,7 +168,7 @@ export const setDocumentTitleMethod = function (newTitle) {
export const setResolutionMethod = function (x, y) {
const currentZoom = elemContext_.getCurrentZoom();
const res = elemContext_.getCanvas().getResolution();
const {w, h} = res;
const { w, h } = res;
let batchCmd;
if (x === 'fit') {
@@ -169,7 +180,7 @@ export const setResolutionMethod = function (x, y) {
const visEls = getVisibleElements();
elemContext_.getCanvas().addToSelection(visEls);
const dx = [], dy = [];
$.each(visEls, function (i, item) {
visEls.forEach(function(_item, _i){
dx.push(bbox.x * -1);
dy.push(bbox.y * -1);
});
@@ -197,10 +208,10 @@ export const setResolutionMethod = function (x, y) {
this.contentW = x;
this.contentH = y;
batchCmd.addSubCommand(new ChangeElementCommand(elemContext_.getSVGContent(), {width: w, height: h}));
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(' ')}));
batchCmd.addSubCommand(new ChangeElementCommand(elemContext_.getSVGContent(), { viewBox: ['0 0', w, h].join(' ') }));
elemContext_.addCommandToHistory(batchCmd);
elemContext_.call('changed', [elemContext_.getSVGContent()]);
@@ -239,13 +250,13 @@ export const setBBoxZoomMethod = function (val, editorW, editorH) {
const selectedElements = elemContext_.getSelectedElements();
let spacer = 0.85;
let bb;
const calcZoom = function (bb) { // eslint-disable-line no-shadow
const calcZoom = function (bb) {
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};
return { zoom, bbox: bb };
};
if (typeof val === 'object') {
@@ -253,35 +264,35 @@ export const setBBoxZoomMethod = function (val, editorW, editorH) {
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 { 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;
}
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;
});
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);
};
@@ -298,7 +309,7 @@ export const setZoomMethod = function (zoomLevel) {
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) {
selectedElements.forEach(function(elem){
if (!elem) { return; }
elemContext_.getCanvas().selectorManager.requestSelector(elem).resize();
});
@@ -318,14 +329,14 @@ export const setZoomMethod = function (zoomLevel) {
export const setColorMethod = function (type, val, preventUndo) {
const selectedElements = elemContext_.getSelectedElements();
elemContext_.setCurShape(type, val);
elemContext_.setCurProperties(type + '_paint', {type: 'solidColor'});
elemContext_.setCurProperties(type + '_paint', { type: 'solidColor' });
const elems = [];
/**
*
* @param {Element} e
* @returns {void}
*/
function addNonG (e) {
function addNonG(e) {
if (e.nodeName !== 'g') {
elems.push(e);
}
@@ -372,7 +383,8 @@ export const setGradientMethod = function (type) {
// no duplicate found, so import gradient into defs
if (!duplicateGrad) {
// const origGrad = grad;
grad = defs.appendChild(elemContext_.getDOMDocument().importNode(grad, true));
grad = elemContext_.getDOMDocument().importNode(grad, true);
defs.append(grad);
// get next id and set it on the grad
grad.id = elemContext_.getCanvas().getNextId();
} else { // use existing gradient
@@ -389,25 +401,37 @@ export const setGradientMethod = function (type) {
*/
export const findDuplicateGradient = function (grad) {
const defs = findDefs();
const existingGrads = $(defs).find('linearGradient, radialGradient');
const existingGrads = defs.querySelectorAll('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')
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);
const gradAttrs = {
r: grad.getAttribute('r'),
cx: grad.getAttribute('cx'),
cy: grad.getAttribute('cy'),
fx: grad.getAttribute('fx'),
fy: grad.getAttribute('fy')
};
const ogAttrs = {
r: og.getAttribute('r'),
cx: og.getAttribute('cx'),
cy: og.getAttribute('cy'),
fx: og.getAttribute('fx'),
fy: og.getAttribute('fy')
};
let diff = false;
$.each(radAttrs, function (j, attr) {
radAttrs.forEach(function (attr) {
if (gradAttrs[attr] !== ogAttrs[attr]) { diff = true; }
});
@@ -428,8 +452,8 @@ export const findDuplicateGradient = function (grad) {
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')) {
stop.getAttribute('stop-opacity') !== ostop.getAttribute('stop-opacity') ||
stop.getAttribute('stop-color') !== ostop.getAttribute('stop-color')) {
break;
}
}
@@ -451,20 +475,20 @@ export const findDuplicateGradient = function (grad) {
*/
export const setPaintMethod = function (type, paint) {
// make a copy
const p = new $.jGraduate.Paint(paint);
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;
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;
}
};
/**
@@ -489,7 +513,8 @@ export const setStrokeWidthMethod = function (val) {
* @param {Element} e
* @returns {void}
*/
function addNonG (e) {
// eslint-disable-next-line sonarjs/no-identical-functions
function addNonG(e) {
if (e.nodeName !== 'g') {
elems.push(e);
}
@@ -550,7 +575,7 @@ export const getBoldMethod = function () {
// should only have one element selected
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
isNullish(selectedElements[1])) {
return (selected.getAttribute('font-weight') === 'bold');
}
return false;
@@ -566,7 +591,7 @@ export const setBoldMethod = function (b) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('font-weight', b ? 'bold' : 'normal');
}
if (!selectedElements[0].textContent) {
@@ -583,7 +608,7 @@ export const getItalicMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
isNullish(selectedElements[1])) {
return (selected.getAttribute('font-style') === 'italic');
}
return false;
@@ -599,7 +624,7 @@ export const setItalicMethod = function (i) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('font-style', i ? 'italic' : 'normal');
}
if (!selectedElements[0].textContent) {
@@ -608,68 +633,7 @@ isNullish(selectedElements[1])) {
};
/**
* Checks whether the selected element has the given text decoration value or not
* @function module:svgcanvas.SvgCanvas#hasTextDecoration
* @param {string} value - The value that should be checked
* @returns {boolean} Indicates whether or not element has the text decoration value
*/
export const hasTextDecorationMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' && isNullish(selectedElements[1])) {
const attribute = selected.getAttribute('text-decoration');
if (attribute) {
return attribute.includes(value);
}
}
return false;
};
/**
* Adds the given value to the text decoration
* @function module:svgcanvas.SvgCanvas#addTextDecoration
* @param {string} value - The value that should be added
* @returns {void}
*/
export const addTextDecorationMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' && isNullish(selectedElements[1])) {
const oldValue = selected.getAttribute('text-decoration') || '';
elemContext_.getCanvas().changeSelectedAttribute('text-decoration', oldValue + ' ' + value);
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* Removes the given value from the text decoration
* @function module:svgcanvas.SvgCanvas#removeTextDecoration
* @param {string} value - The value that should be removed
* @returns {void}
*/
export const removeTextDecorationMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' && isNullish(selectedElements[1])) {
const actualValues = selected.getAttribute('text-decoration');
elemContext_.getCanvas().changeSelectedAttribute('text-decoration', actualValues.replace(value, ''));
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* Set the new text anchor
* @function module:svgcanvas.SvgCanvas#setTextAnchor
* @function module:svgcanvas.SvgCanvas#setTextAnchorMethod Set the new text anchor
* @param {string} value - The text anchor value (start, middle or end)
* @returns {void}
*/
@@ -677,7 +641,7 @@ export const setTextAnchorMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('text-anchor', value);
}
if (!selectedElements[0].textContent) {
@@ -685,134 +649,6 @@ export const setTextAnchorMethod = function (value) {
}
};
/**
* Returns the letter spacing value
* @function module:svgcanvas.SvgCanvas#getLetterSpacing
* @returns {string} The letter spacing value
*/
export const getLetterSpacingMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' && isNullish(selectedElements[1])) {
return selected.getAttribute('letter-spacing') || 0;
}
return null;
};
/**
* Set the new letter spacing.
* @function module:svgcanvas.SvgCanvas#setLetterSpacing
* @param {string} value - The letter spacing
* @returns {void}
*/
export const setLetterSpacingMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('letter-spacing', value);
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* Returns the word spacing value
* @function module:svgcanvas.SvgCanvas#getWordSpacing
* @returns {string} The word spacing value
*/
export const getWordSpacingMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' && isNullish(selectedElements[1])) {
return selected.getAttribute('word-spacing') || 0;
}
return null;
};
/**
* Set the new word spacing.
* @function module:svgcanvas.SvgCanvas#setWordSpacing
* @param {string} value - The word spacing
* @returns {void}
*/
export const setWordSpacingMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('word-spacing', value);
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* Returns the text length value
* @function module:svgcanvas.SvgCanvas#getTextLength
* @returns {string} The text length value
*/
export const getTextLengthMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' && isNullish(selectedElements[1])) {
return selected.getAttribute('textLength') || 0;
}
return null;
};
/**
* Set the new text length.
* @function module:svgcanvas.SvgCanvas#setTextLength
* @param {string} value - The text length
* @returns {void}
*/
export const setTextLengthMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('textLength', value);
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* Returns the length adjust value
* @function module:svgcanvas.SvgCanvas#getLengthAdjust
* @returns {string} The length adjust value
*/
export const getLengthAdjustMethod = function () {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' && isNullish(selectedElements[1])) {
return selected.getAttribute('lengthAdjust') || 0;
}
return null;
};
/**
* Set the new length adjust.
* @function module:svgcanvas.SvgCanvas#setLengthAdjust
* @param {string} value - The length adjust
* @returns {void}
*/
export const setLengthAdjustMethod = function (value) {
const selectedElements = elemContext_.getSelectedElements();
const selected = selectedElements[0];
if (!isNullish(selected) && selected.tagName === 'text' &&
isNullish(selectedElements[1])) {
elemContext_.getCanvas().changeSelectedAttribute('lengthAdjust', value);
}
if (!selectedElements[0].textContent) {
elemContext_.getCanvas().textActions.setCursor();
}
};
/**
* @function module:svgcanvas.SvgCanvas#getFontFamily
* @returns {string} The current font family
@@ -914,7 +750,10 @@ export const setImageURLMethod = function (val) {
const elem = selectedElements[0];
if (!elem) { return; }
const attrs = $(elem).attr(['width', 'height']);
const attrs = {
width: elem.getAttribute('width'),
height: elem.getAttribute('height'),
};
const setsize = (!attrs.width || !attrs.height);
const curHref = getHref(elem);
@@ -930,21 +769,22 @@ export const setImageURLMethod = function (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
});
const img = new Image();
img.onload = function () {
const changes = {
width: elem.getAttribute('width'),
height: elem.getAttribute('height'),
};
elem.setAttribute('width', this.width);
elem.setAttribute('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);
};
img.src = val;
};
/**
@@ -959,7 +799,7 @@ export const setLinkURLMethod = function (val) {
if (!elem) { return; }
if (elem.tagName !== 'a') {
// See if parent is an anchor
const parentsA = $(elem).parents('a');
const parentsA = getParents(elem.parentNode, 'a');
if (parentsA.length) {
elem = parentsA[0];
} else {
@@ -997,7 +837,7 @@ export const setRectRadiusMethod = function (val) {
if (r !== String(val)) {
selected.setAttribute('rx', val);
selected.setAttribute('ry', val);
elemContext_.addCommandToHistory(new ChangeElementCommand(selected, {rx: r, ry: r}, 'Radius'));
elemContext_.addCommandToHistory(new ChangeElementCommand(selected, { rx: r, ry: r }, 'Radius'));
elemContext_.call('changed', [selected]);
}
}
@@ -1047,7 +887,7 @@ export const setSegTypeMethod = function (newType) {
*/
export const setBackgroundMethod = function (color, url) {
const bg = getElem('canvasBackground');
const border = $(bg).find('rect')[0];
const border = bg.querySelector('rect');
let bgImg = getElem('background_image');
let bgPattern = getElem('background_pattern');
border.setAttribute('fill', color === 'chessboard' ? '#fff' : color);
@@ -1064,11 +904,11 @@ export const setBackgroundMethod = function (color, url) {
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);'
'background-image:url(data:image/gif;base64,' +
'R0lGODlhEAAQAIAAAP///9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG+' +
'gq4jM3IFLJgpswNly/XkcBpIiVaInlLJr9FZWAQA7);'
});
bgPattern.appendChild(div);
bgPattern.append(div);
bg.append(bgPattern);
}
} else if (bgPattern) {

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@
* @copyright 2010 Jeff Schiller
*/
import {getHref, setHref, getRotationAngle, isNullish} from '../common/utilities.js';
import {removeElementFromListMap} from '../common/svgtransformlist.js';
import {getHref, setHref, getRotationAngle, isNullish} from './utilities.js';
import {removeElementFromListMap} from './svgtransformlist.js';
/**
* Group: Undo/Redo history management.
@@ -260,10 +260,8 @@ export class RemoveElementCommand extends Command {
unapply (handler) {
super.unapply(handler, () => {
removeElementFromListMap(this.elem);
if (isNullish(this.nextSibling)) {
if (window.console) {
console.error('Reference element was lost');
}
if (isNullish(this.nextSibling) && window.console) {
console.error('Reference element was lost');
}
this.parent.insertBefore(this.elem, this.nextSibling); // Don't use `before` or `prepend` as `this.nextSibling` may be `null`
});

View File

@@ -0,0 +1,79 @@
/**
* A jQuery module to work with SVG attributes.
* @module jQueryAttr
* @license MIT
*/
/**
* This fixes `$(...).attr()` to work as expected with SVG elements.
* Does not currently use `*AttributeNS()` since we rarely need that.
* Adds {@link external:jQuery.fn.attr}.
* See {@link https://api.jquery.com/attr/} for basic documentation of `.attr()`.
*
* Additional functionality:
* - When getting attributes, a string that's a number is returned as type number.
* - If an array is supplied as the first parameter, multiple values are returned
* as an object with values for each given attribute.
* @function module:jQueryAttr.jQueryAttr
* @param {external:jQuery} $ The jQuery object to which to add the plug-in
* @returns {external:jQuery}
*/
export default function jQueryPluginSVG ($) {
const proxied = $.fn.attr,
svgns = 'http://www.w3.org/2000/svg';
/**
* @typedef {PlainObject<string, string|Float>} module:jQueryAttr.Attributes
*/
/**
* @function external:jQuery.fn.attr
* @param {string|string[]|PlainObject<string, string>} key
* @param {string} value
* @returns {external:jQuery|module:jQueryAttr.Attributes}
*/
$.fn.attr = function (key, value) {
const len = this.length;
if (!len) { return proxied.call(this, key, value); }
for (let i = 0; i < len; ++i) {
const elem = this[i];
// set/get SVG attribute
if (elem.namespaceURI === svgns) {
// Setting attribute
if (value !== undefined) {
elem.setAttribute(key, value);
} else if (Array.isArray(key)) {
// Getting attributes from array
const obj = {};
let j = key.length;
while (j--) {
const aname = key[j];
let attr = elem.getAttribute(aname);
// This returns a number when appropriate
if (attr || attr === '0') {
attr = isNaN(attr) ? attr : (attr - 0);
}
obj[aname] = attr;
}
return obj;
}
if (typeof key === 'object') {
// Setting attributes from object
for (const [name, val] of Object.entries(key)) {
elem.setAttribute(name, val);
}
// Getting attribute
} else {
let attr = elem.getAttribute(key);
if (attr || attr === '0') {
attr = isNaN(attr) ? attr : (attr - 0);
}
return attr;
}
} else {
return proxied.call(this, key, value);
}
}
return this;
};
return $;
}

View File

@@ -5,7 +5,7 @@
*
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import {getElem, assignAttributes, cleanupElement} from '../common/utilities.js';
import {getElem, assignAttributes, cleanupElement} from './utilities.js';
import {NS} from '../common/namespaces.js';
let jsonContext_ = null;

229
src/svgcanvas/layer.js Normal file
View File

@@ -0,0 +1,229 @@
/**
* Provides tools for the layer concept.
* @module layer
* @license MIT
*
* @copyright 2011 Jeff Schiller, 2016 Flint O'Brien
*/
import {NS} from '../common/namespaces.js';
import {toXml, walkTree, isNullish} from './utilities.js';
/**
* This class encapsulates the concept of a layer in the drawing. It can be constructed with
* an existing group element or, with three parameters, will create a new layer group element.
*
* @example
* const l1 = new Layer('name', group); // Use the existing group for this layer.
* const l2 = new Layer('name', group, svgElem); // Create a new group and add it to the DOM after group.
* const l3 = new Layer('name', null, svgElem); // Create a new group and add it to the DOM as the last layer.
* @memberof module:layer
*/
class Layer {
/**
* @param {string} name - Layer name
* @param {SVGGElement|null} group - An existing SVG group element or null.
* If group and no svgElem, use group for this layer.
* If group and svgElem, create a new group element and insert it in the DOM after group.
* If no group and svgElem, create a new group element and insert it in the DOM as the last layer.
* @param {SVGGElement} [svgElem] - The SVG DOM element. If defined, use this to add
* a new layer to the document.
*/
constructor (name, group, svgElem) {
this.name_ = name;
this.group_ = svgElem ? null : group;
if (svgElem) {
// Create a group element with title and add it to the DOM.
const svgdoc = svgElem.ownerDocument;
this.group_ = svgdoc.createElementNS(NS.SVG, 'g');
const layerTitle = svgdoc.createElementNS(NS.SVG, 'title');
layerTitle.textContent = name;
this.group_.append(layerTitle);
if (group) {
group.insertAdjacentElement('afterend', this.group_);
} else {
svgElem.append(this.group_);
}
}
addLayerClass(this.group_);
walkTree(this.group_, function (e) {
e.setAttribute('style', 'pointer-events:inherit');
});
this.group_.setAttribute('style', svgElem ? 'pointer-events:all' : 'pointer-events:none');
}
/**
* Get the layer's name.
* @returns {string} The layer name
*/
getName () {
return this.name_;
}
/**
* Get the group element for this layer.
* @returns {SVGGElement} The layer SVG group
*/
getGroup () {
return this.group_;
}
/**
* Active this layer so it takes pointer events.
* @returns {void}
*/
activate () {
this.group_.setAttribute('style', 'pointer-events:all');
}
/**
* Deactive this layer so it does NOT take pointer events.
* @returns {void}
*/
deactivate () {
this.group_.setAttribute('style', 'pointer-events:none');
}
/**
* Set this layer visible or hidden based on 'visible' parameter.
* @param {boolean} visible - If true, make visible; otherwise, hide it.
* @returns {void}
*/
setVisible (visible) {
const expected = visible === undefined || visible ? 'inline' : 'none';
const oldDisplay = this.group_.getAttribute('display');
if (oldDisplay !== expected) {
this.group_.setAttribute('display', expected);
}
}
/**
* Is this layer visible?
* @returns {boolean} True if visible.
*/
isVisible () {
return this.group_.getAttribute('display') !== 'none';
}
/**
* Get layer opacity.
* @returns {Float} Opacity value.
*/
getOpacity () {
const opacity = this.group_.getAttribute('opacity');
if (isNullish(opacity)) {
return 1;
}
return Number.parseFloat(opacity);
}
/**
* Sets the opacity of this layer. If opacity is not a value between 0.0 and 1.0,
* nothing happens.
* @param {Float} opacity - A float value in the range 0.0-1.0
* @returns {void}
*/
setOpacity (opacity) {
if (typeof opacity === 'number' && opacity >= 0.0 && opacity <= 1.0) {
this.group_.setAttribute('opacity', opacity);
}
}
/**
* Append children to this layer.
* @param {SVGGElement} children - The children to append to this layer.
* @returns {void}
*/
appendChildren (children) {
for (const child of children) {
this.group_.append(child);
}
}
/**
* @returns {SVGTitleElement|null}
*/
getTitleElement () {
const len = this.group_.childNodes.length;
for (let i = 0; i < len; ++i) {
const child = this.group_.childNodes.item(i);
if (child && child.tagName === 'title') {
return child;
}
}
return null;
}
/**
* Set the name of this layer.
* @param {string} name - The new name.
* @param {module:history.HistoryRecordingService} hrService - History recording service
* @returns {string|null} The new name if changed; otherwise, null.
*/
setName (name, hrService) {
const previousName = this.name_;
name = toXml(name);
// now change the underlying title element contents
const title = this.getTitleElement();
if (title) {
while(title.firstChild)
title.removeChild(title.firstChild);
title.textContent = name;
this.name_ = name;
if (hrService) {
hrService.changeElement(title, {'#text': previousName});
}
return this.name_;
}
return null;
}
/**
* Remove this layer's group from the DOM. No more functions on group can be called after this.
* @returns {SVGGElement} The layer SVG group that was just removed.
*/
removeGroup () {
const group = this.group_;
this.group_.remove();
this.group_ = undefined;
return group;
}
/**
* Test whether an element is a layer or not.
* @param {SVGGElement} elem - The SVGGElement to test.
* @returns {boolean} True if the element is a layer
*/
static isLayer (elem) {
return elem && elem.tagName === 'g' && Layer.CLASS_REGEX.test(elem.getAttribute('class'));
}
}
/**
* @property {string} CLASS_NAME - class attribute assigned to all layer groups.
*/
Layer.CLASS_NAME = 'layer';
/**
* @property {RegExp} CLASS_REGEX - Used to test presence of class Layer.CLASS_NAME
*/
Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)');
/**
* Add class `Layer.CLASS_NAME` to the element (usually `class='layer'`).
*
* @param {SVGGElement} elem - The SVG element to update
* @returns {void}
*/
function addLayerClass (elem) {
const classes = elem.getAttribute('class');
if (isNullish(classes) || !classes.length) {
elem.setAttribute('class', Layer.CLASS_NAME);
} else if (!Layer.CLASS_REGEX.test(classes)) {
elem.setAttribute('class', classes + ' ' + Layer.CLASS_NAME);
}
}
export default Layer;

222
src/svgcanvas/math.js Normal file
View File

@@ -0,0 +1,222 @@
/**
* Mathematical utilities.
* @module math
* @license MIT
*
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
/**
* @typedef {PlainObject} module:math.AngleCoord45
* @property {Float} x - The angle-snapped x value
* @property {Float} y - The angle-snapped y value
* @property {Integer} a - The angle at which to snap
*/
/**
* @typedef {PlainObject} module:math.XYObject
* @property {Float} x
* @property {Float} y
*/
import {NS} from '../common/namespaces.js';
import {getTransformList} from './svgtransformlist.js';
// Constants
const NEAR_ZERO = 1e-14;
// Throw away SVGSVGElement used for creating matrices/transforms.
const svg = document.createElementNS(NS.SVG, 'svg');
/**
* A (hopefully) quicker function to transform a point by a matrix
* (this function avoids any DOM calls and just does the math).
* @function module:math.transformPoint
* @param {Float} x - Float representing the x coordinate
* @param {Float} y - Float representing the y coordinate
* @param {SVGMatrix} m - Matrix object to transform the point with
* @returns {module:math.XYObject} An x, y object representing the transformed point
*/
export const transformPoint = function (x, y, m) {
return {x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f};
};
/**
* Helper function to check if the matrix performs no actual transform
* (i.e. exists for identity purposes).
* @function module:math.isIdentity
* @param {SVGMatrix} m - The matrix object to check
* @returns {boolean} Indicates whether or not the matrix is 1,0,0,1,0,0
*/
export const isIdentity = function (m) {
return (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0);
};
/**
* This function tries to return a `SVGMatrix` that is the multiplication `m1 * m2`.
* We also round to zero when it's near zero.
* @function module:math.matrixMultiply
* @param {...SVGMatrix} args - Matrix objects to multiply
* @returns {SVGMatrix} The matrix object resulting from the calculation
*/
export const matrixMultiply = function (...args) {
const m = args.reduceRight((prev, m1) => {
return m1.multiply(prev);
});
if (Math.abs(m.a) < NEAR_ZERO) { m.a = 0; }
if (Math.abs(m.b) < NEAR_ZERO) { m.b = 0; }
if (Math.abs(m.c) < NEAR_ZERO) { m.c = 0; }
if (Math.abs(m.d) < NEAR_ZERO) { m.d = 0; }
if (Math.abs(m.e) < NEAR_ZERO) { m.e = 0; }
if (Math.abs(m.f) < NEAR_ZERO) { m.f = 0; }
return m;
};
/**
* See if the given transformlist includes a non-indentity matrix transform.
* @function module:math.hasMatrixTransform
* @param {SVGTransformList} [tlist] - The transformlist to check
* @returns {boolean} Whether or not a matrix transform was found
*/
export const hasMatrixTransform = function (tlist) {
if (!tlist) { return false; }
let num = tlist.numberOfItems;
while (num--) {
const xform = tlist.getItem(num);
if (xform.type === 1 && !isIdentity(xform.matrix)) { return true; }
}
return false;
};
/**
* @typedef {PlainObject} module:math.TransformedBox An object with the following values
* @property {module:math.XYObject} tl - The top left coordinate
* @property {module:math.XYObject} tr - The top right coordinate
* @property {module:math.XYObject} bl - The bottom left coordinate
* @property {module:math.XYObject} br - The bottom right coordinate
* @property {PlainObject} aabox - Object with the following values:
* @property {Float} aabox.x - Float with the axis-aligned x coordinate
* @property {Float} aabox.y - Float with the axis-aligned y coordinate
* @property {Float} aabox.width - Float with the axis-aligned width coordinate
* @property {Float} aabox.height - Float with the axis-aligned height coordinate
*/
/**
* Transforms a rectangle based on the given matrix.
* @function module:math.transformBox
* @param {Float} l - Float with the box's left coordinate
* @param {Float} t - Float with the box's top coordinate
* @param {Float} w - Float with the box width
* @param {Float} h - Float with the box height
* @param {SVGMatrix} m - Matrix object to transform the box by
* @returns {module:math.TransformedBox}
*/
export const transformBox = function (l, t, w, h, m) {
const tl = transformPoint(l, t, m),
tr = transformPoint((l + w), t, m),
bl = transformPoint(l, (t + h), m),
br = transformPoint((l + w), (t + h), m),
minx = Math.min(tl.x, tr.x, bl.x, br.x),
maxx = Math.max(tl.x, tr.x, bl.x, br.x),
miny = Math.min(tl.y, tr.y, bl.y, br.y),
maxy = Math.max(tl.y, tr.y, bl.y, br.y);
return {
tl,
tr,
bl,
br,
aabox: {
x: minx,
y: miny,
width: (maxx - minx),
height: (maxy - miny)
}
};
};
/**
* This returns a single matrix Transform for a given Transform List
* (this is the equivalent of `SVGTransformList.consolidate()` but unlike
* that method, this one does not modify the actual `SVGTransformList`).
* This function is very liberal with its `min`, `max` arguments.
* @function module:math.transformListToTransform
* @param {SVGTransformList} tlist - The transformlist object
* @param {Integer} [min=0] - Optional integer indicating start transform position
* @param {Integer} [max] - Optional integer indicating end transform position;
* defaults to one less than the tlist's `numberOfItems`
* @returns {SVGTransform} A single matrix transform object
*/
export const transformListToTransform = function (tlist, min, max) {
if (!tlist) {
// Or should tlist = null have been prevented before this?
return svg.createSVGTransformFromMatrix(svg.createSVGMatrix());
}
min = min || 0;
max = max || (tlist.numberOfItems - 1);
min = Number.parseInt(min);
max = Number.parseInt(max);
if (min > max) { const temp = max; max = min; min = temp; }
let m = svg.createSVGMatrix();
for (let i = min; i <= max; ++i) {
// if our indices are out of range, just use a harmless identity matrix
const mtom = (i >= 0 && i < tlist.numberOfItems
? tlist.getItem(i).matrix
: svg.createSVGMatrix());
m = matrixMultiply(m, mtom);
}
return svg.createSVGTransformFromMatrix(m);
};
/**
* Get the matrix object for a given element.
* @function module:math.getMatrix
* @param {Element} elem - The DOM element to check
* @returns {SVGMatrix} The matrix object associated with the element's transformlist
*/
export const getMatrix = function (elem) {
const tlist = getTransformList(elem);
return transformListToTransform(tlist).matrix;
};
/**
* Returns a 45 degree angle coordinate associated with the two given
* coordinates.
* @function module:math.snapToAngle
* @param {Integer} x1 - First coordinate's x value
* @param {Integer} y1 - First coordinate's y value
* @param {Integer} x2 - Second coordinate's x value
* @param {Integer} y2 - Second coordinate's y value
* @returns {module:math.AngleCoord45}
*/
export const snapToAngle = function (x1, y1, x2, y2) {
const snap = Math.PI / 4; // 45 degrees
const dx = x2 - x1;
const dy = y2 - y1;
const angle = Math.atan2(dy, dx);
const dist = Math.sqrt(dx * dx + dy * dy);
const snapangle = Math.round(angle / snap) * snap;
return {
x: x1 + dist * Math.cos(snapangle),
y: y1 + dist * Math.sin(snapangle),
a: snapangle
};
};
/**
* Check if two rectangles (BBoxes objects) intersect each other.
* @function module:math.rectsIntersect
* @param {SVGRect} r1 - The first BBox-like object
* @param {SVGRect} r2 - The second BBox-like object
* @returns {boolean} True if rectangles intersect
*/
export const rectsIntersect = function (r1, r2) {
return r2.x < (r1.x + r1.width) &&
(r2.x + r2.width) > r1.x &&
r2.y < (r1.y + r1.height) &&
(r2.y + r2.height) > r1.y;
};

View File

@@ -1,12 +1,7 @@
/* 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';
} from './utilities.js';
import * as hstry from './history.js';
// Constants
const $ = jQueryPluginSVG(jQuery);
const {
InsertElementCommand, BatchCommand
@@ -118,7 +113,7 @@ export const pasteElementsMethod = function (type, x, y) {
dx = [],
dy = [];
$.each(pasted, function (i, item) {
pasted.forEach(function(_item){
dx.push(cx);
dy.push(cy);
});

View File

@@ -9,16 +9,16 @@
import {NS} from '../common/namespaces.js';
import {shortFloat} from '../common/units.js';
import {getTransformList} from '../common/svgtransformlist.js';
import {getTransformList} from './svgtransformlist.js';
import {ChangeElementCommand, BatchCommand} from './history.js';
import {
transformPoint, snapToAngle, rectsIntersect,
transformListToTransform
} from '../common/math.js';
} from './math.js';
import {
assignAttributes, getElem, getRotationAngle, snapToGrid, isNullish,
getBBox as utilsGetBBox
} from '../common/utilities.js';
} from './utilities.js';
import {
isWebkit
} from '../common/browser.js';
@@ -114,6 +114,7 @@ export const convertPath = function (pth, toRel) {
case 2: // absolute move (M)
case 4: // absolute line (L)
case 18: // absolute smooth quad (T)
case 10: // absolute elliptical arc (A)
x -= curx;
y -= cury;
// Fallthrough
@@ -165,10 +166,6 @@ export const convertPath = function (pth, toRel) {
}
d += pathDSegment(letter, [[x1, y1], [x, y]]);
break;
// eslint-disable-next-line sonarjs/no-duplicated-branches
case 10: // absolute elliptical arc (A)
x -= curx;
y -= cury;
// Fallthrough
case 11: // relative elliptical arc (a)
if (toRel) {
@@ -217,7 +214,7 @@ export const convertPath = function (pth, toRel) {
* @returns {string}
*/
function pathDSegment (letter, points, morePoints, lastPoint) {
$.each(points, function (i, pnt) {
points.forEach(function(pnt, i){
points[i] = shortFloat(pnt);
});
let segment = letter + points.join(' ');
@@ -230,7 +227,6 @@ function pathDSegment (letter, points, morePoints, lastPoint) {
return segment;
}
/* eslint-disable jsdoc/require-property */
/**
* Group: Path edit functions.
* Functions relating to editing path elements.
@@ -238,7 +234,6 @@ function pathDSegment (letter, points, morePoints, lastPoint) {
* @memberof module:path
*/
export const pathActionsMethod = (function () {
/* eslint-enable jsdoc/require-property */
let subpath = false;
let newPoint, firstCtrl;
@@ -361,7 +356,7 @@ export const pathActionsMethod = (function () {
'stroke-width': '0.5',
fill: 'none'
});
stretchy = getElem('selectorParentGroup').appendChild(stretchy);
getElem('selectorParentGroup').append(stretchy);
}
stretchy.setAttribute('display', 'inline');
@@ -437,7 +432,7 @@ export const pathActionsMethod = (function () {
keep = false;
return keep;
}
$(stretchy).remove();
stretchy.remove();
// This will signal to commit the path
// const element = newpath; // Other event handlers define own `element`, so this was probably not meant to interact with them or one which shares state (as there were none); I therefore adding a missing `var` to avoid a global
@@ -450,9 +445,9 @@ export const pathActionsMethod = (function () {
}
const newD = newpath.getAttribute('d');
const origD = $(path.elem).attr('d');
$(path.elem).attr('d', origD + newD);
$(newpath).remove();
const origD = path.elem.getAttribute('d');
path.elem.setAttribute('d', origD + newD);
newpath.parentNode.removeChild();
if (path.matrix) {
pathActionsContext_.recalcRotatedPath();
}
@@ -675,7 +670,7 @@ export const pathActionsMethod = (function () {
}
} else {
path.selected_pts = [];
path.eachSeg(function (i) {
path.eachSeg(function (_i) {
const seg = this;
if (!seg.next && !seg.prev) { return; }
@@ -708,11 +703,11 @@ export const pathActionsMethod = (function () {
/**
* @param {Event} evt
* @param {Element} element
* @param {Float} mouseX
* @param {Float} mouseY
* @param {Float} _mouseX
* @param {Float} _mouseY
* @returns {module:path.keepElement|void}
*/
mouseUp (evt, element, mouseX, mouseY) {
mouseUp (evt, element, _mouseX, _mouseY) {
editorContext_ = pathActionsContext_.getEditorContext();
const drawnPath = editorContext_.getDrawnPath();
// Create mode
@@ -862,15 +857,20 @@ export const pathActionsMethod = (function () {
* @param {boolean} remove Not in use
* @returns {void}
*/
clear (remove) {
clear () {
editorContext_ = pathActionsContext_.getEditorContext();
const drawnPath = editorContext_.getDrawnPath();
currentPath = null;
if (drawnPath) {
const elem = getElem(editorContext_.getId());
$(getElem('path_stretch_line')).remove();
$(elem).remove();
$(getElem('pathpointgrip_container')).find('*').attr('display', 'none');
const psl = getElem('path_stretch_line');
psl.parentNode.removeChild(psl);
elem.parentNode.removeChild(elem);
const pathpointgripContainer = getElem('pathpointgrip_container');
const elements = pathpointgripContainer.querySelectorAll('*');
Array.prototype.forEach.call(elements, function(el){
el.style.display = 'none';
});
firstCtrl = null;
editorContext_.setDrawnPath(null);
editorContext_.setStarted(false);
@@ -909,7 +909,7 @@ export const pathActionsMethod = (function () {
const type = seg.pathSegType;
if (type === 1) { continue; }
const pts = [];
$.each(['', 1, 2], function (j, n) {
['', 1, 2].forEach(function(n){
const x = seg['x' + n], y = seg['y' + n];
if (x !== undefined && y !== undefined) {
const pt = transformPoint(x, y, m);
@@ -1134,20 +1134,18 @@ export const pathActionsMethod = (function () {
cleanup();
break;
}
} else if (item.pathSegType === 2) {
if (len > 0) {
const prevType = segList.getItem(len - 1).pathSegType;
// Path has M M
if (prevType === 2) {
remItems(len - 1, 1);
cleanup();
break;
} else if (item.pathSegType === 2 && len > 0) {
const prevType = segList.getItem(len - 1).pathSegType;
// Path has M M
if (prevType === 2) {
remItems(len - 1, 1);
cleanup();
break;
// Entire path ends with Z M
} else if (prevType === 1 && segList.numberOfItems - 1 === len) {
remItems(len, 1);
cleanup();
break;
}
} else if (prevType === 1 && segList.numberOfItems - 1 === len) {
remItems(len, 1);
cleanup();
break;
}
}
}
@@ -1170,12 +1168,10 @@ export const pathActionsMethod = (function () {
// TODO: Find right way to select point now
// path.selectPt(selPt);
if (window.opera) { // Opera repaints incorrectly
const cp = $(path.elem);
cp.attr('d', cp.attr('d'));
path.elem.setAttribute('d', path.elem.getAttribute('d'));
}
path.endChanges('Delete path node(s)');
},
/* eslint-disable jsdoc/require-returns */
// Can't seem to use `@borrows` here, so using `@see`
/**
* Smooth polyline into path.
@@ -1183,7 +1179,7 @@ export const pathActionsMethod = (function () {
* @see module:path~smoothPolylineIntoPath
*/
smoothPolylineIntoPath,
/* eslint-enable jsdoc/require-returns */
/* eslint-enable */
/**
* @param {?Integer} v See {@link https://www.w3.org/TR/SVG/single-page.html#paths-InterfaceSVGPathSeg}
* @returns {void}
@@ -1242,7 +1238,6 @@ export const pathActionsMethod = (function () {
editorContext_ = pathActionsContext_.getEditorContext();
if (isWebkit()) { editorContext_.resetD(elem); }
},
/* eslint-disable jsdoc/require-returns */
// Can't seem to use `@borrows` here, so using `@see`
/**
* Convert a path to one with only absolute or relative values.
@@ -1250,7 +1245,6 @@ export const pathActionsMethod = (function () {
* @see module:path.convertPath
*/
convertPath
/* eslint-enable jsdoc/require-returns */
});
})();
// end pathActions

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* Path functionality.
* @module path
@@ -11,16 +10,15 @@ import {NS} from '../common/namespaces.js';
import {ChangeElementCommand} from './history.js';
import {
transformPoint, getMatrix
} from '../common/math.js';
} from './math.js';
import {
assignAttributes, getRotationAngle, isNullish,
getElem
} from '../common/utilities.js';
} from './utilities.js';
import {
supportsPathInsertItemBefore, supportsPathReplaceItem, isWebkit
} from '../common/browser.js';
const $ = jQuery;
let pathMethodsContext_ = null;
let editorContext_ = null;
@@ -134,7 +132,8 @@ export const getGripContainerMethod = function () {
let c = getElem('pathpointgrip_container');
if (!c) {
const parentElement = getElem('selectorParentGroup');
c = parentElement.appendChild(document.createElementNS(NS.SVG, 'g'));
c = document.createElementNS(NS.SVG, 'g');
parentElement.append(c);
c.id = 'pathpointgrip_container';
}
return c;
@@ -171,10 +170,10 @@ export const addPointGripMethod = function (index, x, y) {
atts['xlink:title'] = uiStrings.pathNodeTooltip;
}
assignAttributes(pointGrip, atts);
pointGrip = pointGripContainer.appendChild(pointGrip);
pointGripContainer.append(pointGrip);
const grip = $('#pathpointgrip_' + index);
grip.dblclick(function () {
const grip = document.getElementById('pathpointgrip_' + index);
grip?.addEventListener("dblclick", () => {
const path = pathMethodsContext_.getPathObj();
if (path) {
path.setSegType();
@@ -428,8 +427,8 @@ export class Segment {
* @returns {void}
*/
selectCtrls (y) {
$('#ctrlpointgrip_' + this.index + 'c1, #ctrlpointgrip_' + this.index + 'c2')
.attr('fill', y ? '#0FF' : '#EEE');
document.getElementById('ctrlpointgrip_' + this.index + 'c1').setAttribute('fill', y ? '#0FF' : '#EEE');
document.getElementById('ctrlpointgrip_' + this.index + 'c2').setAttribute('fill', y ? '#0FF' : '#EEE')
}
/**
@@ -637,8 +636,10 @@ export class Path {
// Hide all grips, etc
// fixed, needed to work on all found elements, not just first
$(getGripContainerMethod()).find('*').each(function () {
$(this).attr('display', 'none');
const pointGripContainer = getGripContainerMethod();
const elements = pointGripContainer.querySelectorAll('*');
Array.prototype.forEach.call(elements, function(el){
el.style.display = 'none';
});
const segList = this.elem.pathSegList;
@@ -1012,10 +1013,8 @@ export class Path {
if (!Array.isArray(indexes)) { indexes = [indexes]; }
indexes.forEach((index) => {
const seg = this.segs[index];
if (seg.ptgrip) {
if (!this.selected_pts.includes(index) && index >= 0) {
this.selected_pts.push(index);
}
if (seg.ptgrip && !this.selected_pts.includes(index) && index >= 0) {
this.selected_pts.push(index);
}
});
this.selected_pts.sort();

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* Path functionality.
* @module path
@@ -7,14 +6,14 @@
* @copyright 2011 Alexis Deveria, 2011 Jeff Schiller
*/
import {getTransformList} from '../common/svgtransformlist.js';
import {getTransformList} from './svgtransformlist.js';
import {shortFloat} from '../common/units.js';
import {transformPoint} from '../common/math.js';
import {transformPoint} from './math.js';
import {
getRotationAngle, getBBox,
getRefElem, findDefs, isNullish,
getBBox as utilsGetBBox
} from '../common/utilities.js';
} from './utilities.js';
import {
init as pathMethodInit, insertItemBeforeMethod, ptObjToArrMethod, getGripPtMethod,
getPointFromGripMethod, addPointGripMethod, getGripContainerMethod, addCtrlGripMethod,
@@ -25,8 +24,6 @@ import {
init as pathActionsInit, pathActionsMethod
} from './path-actions.js';
const $ = jQuery;
const segData = {
2: ['x', 'y'], // PATHSEG_MOVETO_ABS
4: ['x', 'y'], // PATHSEG_LINETO_ABS
@@ -77,7 +74,7 @@ export const setLinkControlPoints = function (lcp) {
* @type {null|module:path.Path}
* @memberof module:path
*/
export let path = null; // eslint-disable-line import/no-mutable-exports
export let path = null;
let editorContext_ = null;
@@ -244,7 +241,7 @@ export const init = function (editorContext) {
'Moveto', 'Lineto', 'CurvetoCubic', 'CurvetoQuadratic', 'Arc',
'LinetoHorizontal', 'LinetoVertical', 'CurvetoCubicSmooth', 'CurvetoQuadraticSmooth'
];
$.each(pathFuncsStrs, function (i, s) {
pathFuncsStrs.forEach(function(s){
pathFuncs.push(s + 'Abs');
pathFuncs.push(s + 'Rel');
});
@@ -581,8 +578,9 @@ export const reorientGrads = function (elem, m) {
};
const newgrad = grad.cloneNode(true);
$(newgrad).attr(gCoords);
for (const [key, value] of Object.entries(gCoords)) {
newgrad.setAttribute(key, value);
}
newgrad.id = editorContext_.getNextId();
findDefs().append(newgrad);
elem.setAttribute(type, 'url(#' + newgrad.id + ')');
@@ -674,6 +672,7 @@ export const convertPath = function (pth, toRel) {
case 2: // absolute move (M)
case 4: // absolute line (L)
case 18: // absolute smooth quad (T)
case 10: // absolute elliptical arc (A)
x -= curx;
y -= cury;
// Fallthrough
@@ -725,10 +724,6 @@ export const convertPath = function (pth, toRel) {
}
d += pathDSegment(letter, [[x1, y1], [x, y]]);
break;
// eslint-disable-next-line sonarjs/no-duplicated-branches
case 10: // absolute elliptical arc (A)
x -= curx;
y -= cury;
// Fallthrough
case 11: // relative elliptical arc (a)
if (toRel) {
@@ -777,7 +772,7 @@ export const convertPath = function (pth, toRel) {
* @returns {string}
*/
function pathDSegment (letter, points, morePoints, lastPoint) {
$.each(points, function (i, pnt) {
points.forEach(function(pnt, i){
points[i] = shortFloat(pnt);
});
let segment = letter + points.join(' ');

View File

@@ -5,18 +5,18 @@
* @license MIT
*/
import jQueryPluginSVG from '../common/jQuery.attr.js'; // Needed for SVG attribute setting and array form with `attr`
import {NS} from '../common/namespaces.js';
import {convertToNum} from '../common/units.js';
import {isWebkit} from '../common/browser.js';
import {getTransformList} from '../common/svgtransformlist.js';
import {getRotationAngle, getHref, getBBox, getRefElem, isNullish} from '../common/utilities.js';
import {BatchCommand, ChangeElementCommand} from './history.js';
import {remapElement} from './coords.js';
import jQueryPluginSVG from './jQuery.attr.js'; // Needed for SVG attribute setting and array form with `attr`
import { NS } from '../common/namespaces.js';
import { convertToNum } from '../common/units.js';
import { isWebkit } from '../common/browser.js';
import { getTransformList } from './svgtransformlist.js';
import { getRotationAngle, getHref, getBBox, getRefElem, isNullish } from './utilities.js';
import { BatchCommand, ChangeElementCommand } from './history.js';
import { remapElement } from './coords.js';
import {
isIdentity, matrixMultiply, transformPoint, transformListToTransform,
hasMatrixTransform
} from '../common/math.js';
} from './math.js';
const $ = jQueryPluginSVG(jQuery);
@@ -83,6 +83,7 @@ export const recalculateDimensions = function (selected) {
}
const svgroot = context_.getSVGRoot();
const dataStorage = context_.getDataStorage();
const tlist = getTransformList(selected);
// remove any unnecessary transforms
@@ -93,7 +94,7 @@ export const recalculateDimensions = function (selected) {
const xform = tlist.getItem(k);
if (xform.type === 0) {
tlist.removeItem(k);
// remove identity matrices
// remove identity matrices
} else if (xform.type === 1) {
if (isIdentity(xform.matrix)) {
if (noi === 1) {
@@ -106,16 +107,14 @@ export const recalculateDimensions = function (selected) {
}
tlist.removeItem(k);
}
// remove zero-degree rotations
} else if (xform.type === 4) {
if (xform.angle === 0) {
tlist.removeItem(k);
}
// remove zero-degree rotations
} else if (xform.type === 4 && xform.angle === 0) {
tlist.removeItem(k);
}
}
// End here if all it has is a rotation
if (tlist.numberOfItems === 1 &&
getRotationAngle(selected)) { return null; }
getRotationAngle(selected)) { return null; }
}
// if this element had no transforms, we are done
@@ -165,22 +164,20 @@ export const recalculateDimensions = function (selected) {
// If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned).
switch (selected.tagName) {
// Ignore these elements, as they can absorb the [M]
case 'line':
case 'polyline':
case 'polygon':
case 'path':
break;
default:
if ((tlist.numberOfItems === 1 && tlist.getItem(0).type === 1) ||
// Ignore these elements, as they can absorb the [M]
case 'line':
case 'polyline':
case 'polygon':
case 'path':
break;
default:
if ((tlist.numberOfItems === 1 && tlist.getItem(0).type === 1) ||
(tlist.numberOfItems === 2 && tlist.getItem(0).type === 1 && tlist.getItem(0).type === 4)) {
return null;
}
return null;
}
}
// Grouped SVG element
const gsvg = $(selected).data('gsvg');
const gsvg = (dataStorage.has(selected, 'gsvg')) ? dataStorage.get(selected, 'gsvg') : undefined;
// we know we have some transforms, so set up return variable
const batchCmd = new BatchCommand('Transform');
@@ -189,54 +186,56 @@ export const recalculateDimensions = function (selected) {
let initial = null;
let attrs = [];
switch (selected.tagName) {
case 'line':
attrs = ['x1', 'y1', 'x2', 'y2'];
break;
case 'circle':
attrs = ['cx', 'cy', 'r'];
break;
case 'ellipse':
attrs = ['cx', 'cy', 'rx', 'ry'];
break;
case 'foreignObject':
case 'rect':
case 'image':
attrs = ['width', 'height', 'x', 'y'];
break;
case 'use':
case 'text':
case 'tspan':
attrs = ['x', 'y'];
break;
case 'polygon':
case 'polyline': {
initial = {};
initial.points = selected.getAttribute('points');
const list = selected.points;
const len = list.numberOfItems;
changes.points = new Array(len);
for (let i = 0; i < len; ++i) {
const pt = list.getItem(i);
changes.points[i] = {x: pt.x, y: pt.y};
}
break;
} case 'path':
initial = {};
initial.d = selected.getAttribute('d');
changes.d = selected.getAttribute('d');
break;
case 'line':
attrs = ['x1', 'y1', 'x2', 'y2'];
break;
case 'circle':
attrs = ['cx', 'cy', 'r'];
break;
case 'ellipse':
attrs = ['cx', 'cy', 'rx', 'ry'];
break;
case 'foreignObject':
case 'rect':
case 'image':
attrs = ['width', 'height', 'x', 'y'];
break;
case 'use':
case 'text':
case 'tspan':
attrs = ['x', 'y'];
break;
case 'polygon':
case 'polyline': {
initial = {};
initial.points = selected.getAttribute('points');
const list = selected.points;
const len = list.numberOfItems;
changes.points = new Array(len);
for (let i = 0; i < len; ++i) {
const pt = list.getItem(i);
changes.points[i] = { x: pt.x, y: pt.y };
}
break;
} case 'path':
initial = {};
initial.d = selected.getAttribute('d');
changes.d = selected.getAttribute('d');
break;
} // switch on element type to get initial values
if (attrs.length) {
changes = $(selected).attr(attrs);
$.each(changes, function (attr, val) {
changes[attr] = convertToNum(attr, val);
Array.prototype.forEach.call(attrs, function (attr) {
changes[attr] = selected.getAttribute(attr);
});
for (const [attr, val] of Object.entries(changes)) {
changes[attr] = convertToNum(attr, val);
}
} else if (gsvg) {
// GSVG exception
changes = {
x: $(gsvg).attr('x') || 0,
y: $(gsvg).attr('y') || 0
x: gsvg.getAttribute('x') || 0,
y: gsvg.getAttribute('y') || 0
};
}
@@ -244,9 +243,9 @@ export const recalculateDimensions = function (selected) {
// make a copy of initial values and include the transform
if (isNullish(initial)) {
initial = $.extend(true, {}, changes);
$.each(initial, function (attr, val) {
for (const [attr, val] of Object.entries(initial)) {
initial[attr] = convertToNum(attr, val);
});
}
}
// save the start transform value too
initial.transform = context_.getStartTransform() || '';
@@ -257,7 +256,7 @@ export const recalculateDimensions = function (selected) {
if ((selected.tagName === 'g' && !gsvg) || selected.tagName === 'a') {
const box = getBBox(selected);
oldcenter = {x: box.x + box.width / 2, y: box.y + box.height / 2};
oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
newcenter = transformPoint(
box.x + box.width / 2,
box.y + box.height / 2,
@@ -342,7 +341,7 @@ export const recalculateDimensions = function (selected) {
childTlist.clear();
childTlist.appendItem(e2t);
// childxforms.push(e2t);
// if not rotated or skewed, push the [T][S][-T] down to the child
// if not rotated or skewed, push the [T][S][-T] down to the child
} else {
// update the transform list with translate,scale,translate
@@ -407,9 +406,9 @@ export const recalculateDimensions = function (selected) {
e2t.setMatrix(m);
tlist.clear();
tlist.appendItem(e2t);
// next, check if the first transform was a translate
// if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ]
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ]
// next, check if the first transform was a translate
// if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ]
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ]
} else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&
tlist.getItem(0).type === 2) {
operation = 2; // translate
@@ -475,8 +474,8 @@ export const recalculateDimensions = function (selected) {
}
context_.setStartTransform(oldStartTransform);
}
// else, a matrix imposition from a parent group
// keep pushing it down to the children
// else, a matrix imposition from a parent group
// keep pushing it down to the children
} else if (N === 1 && tlist.getItem(0).type === 1 && !gangle) {
operation = 1;
const m = tlist.getItem(0).matrix,
@@ -510,7 +509,7 @@ export const recalculateDimensions = function (selected) {
}
}
tlist.clear();
// else it was just a rotate
// else it was just a rotate
} else {
if (gangle) {
const newRot = svgroot.createSVGTransform();
@@ -543,7 +542,7 @@ export const recalculateDimensions = function (selected) {
tlist.appendItem(newRot);
}
}
// if it was a resize
// if it was a resize
} else if (operation === 3) {
const m = transformListToTransform(tlist).matrix;
const roldt = svgroot.createSVGTransform();
@@ -591,7 +590,7 @@ export const recalculateDimensions = function (selected) {
}
}
}
// else, it's a non-group
// else, it's a non-group
} else {
// TODO: box might be null for some elements (<metadata> etc), need to handle this
const box = getBBox(selected);
@@ -607,7 +606,7 @@ export const recalculateDimensions = function (selected) {
// temporarily strip off the rotate and save the old center
const angle = getRotationAngle(selected);
if (angle) {
oldcenter = {x: box.x + box.width / 2, y: box.y + box.height / 2};
oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
newcenter = transformPoint(
box.x + box.width / 2,
box.y + box.height / 2,
@@ -673,8 +672,8 @@ export const recalculateDimensions = function (selected) {
tlist.removeItem(N - 1);
tlist.removeItem(N - 2);
tlist.removeItem(N - 3);
// if we had [T][S][-T][M], then this was a skewed element being resized
// Thus, we simply combine it all into one matrix
// if we had [T][S][-T][M], then this was a skewed element being resized
// Thus, we simply combine it all into one matrix
} else if (N === 4 && tlist.getItem(N - 1).type === 1) {
operation = 3; // scale
m = transformListToTransform(tlist).matrix;
@@ -684,10 +683,10 @@ export const recalculateDimensions = function (selected) {
tlist.appendItem(e2t);
// reset the matrix so that the element is not re-mapped
m = svgroot.createSVGMatrix();
// if we had [R][T][S][-T][M], then this was a rotated matrix-element
// if we had [T1][M] we want to transform this into [M][T2]
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]
// down to the element
// if we had [R][T][S][-T][M], then this was a rotated matrix-element
// if we had [T1][M] we want to transform this into [M][T2]
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]
// down to the element
} else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&
tlist.getItem(0).type === 2) {
operation = 2; // translate
@@ -696,38 +695,43 @@ export const recalculateDimensions = function (selected) {
meqInv = meq.inverse();
m = matrixMultiply(meqInv, oldxlate, meq);
tlist.removeItem(0);
// else if this child now has a matrix imposition (from a parent group)
// we might be able to simplify
// else if this child now has a matrix imposition (from a parent group)
// we might be able to simplify
} else if (N === 1 && tlist.getItem(0).type === 1 && !angle) {
// Remap all point-based elements
m = transformListToTransform(tlist).matrix;
switch (selected.tagName) {
case 'line':
changes = $(selected).attr(['x1', 'y1', 'x2', 'y2']);
// Fallthrough
case 'polyline':
case 'polygon':
changes.points = selected.getAttribute('points');
if (changes.points) {
const list = selected.points;
const len = list.numberOfItems;
changes.points = new Array(len);
for (let i = 0; i < len; ++i) {
const pt = list.getItem(i);
changes.points[i] = {x: pt.x, y: pt.y};
case 'line':
changes = {
x1: selected.getAttribute('x1'),
y1: selected.getAttribute('y1'),
x2: selected.getAttribute('x2'),
y2: selected.getAttribute('y2'),
}
}
// Fallthrough
case 'path':
changes.d = selected.getAttribute('d');
operation = 1;
tlist.clear();
break;
default:
break;
case 'polyline':
case 'polygon':
changes.points = selected.getAttribute('points');
if (changes.points) {
const list = selected.points;
const len = list.numberOfItems;
changes.points = new Array(len);
for (let i = 0; i < len; ++i) {
const pt = list.getItem(i);
changes.points[i] = { x: pt.x, y: pt.y };
}
}
// Fallthrough
case 'path':
changes.d = selected.getAttribute('d');
operation = 1;
tlist.clear();
break;
default:
break;
}
// if it was a rotation, put the rotate back and return without a command
// (this function has zero work to do for a rotate())
// if it was a rotation, put the rotate back and return without a command
// (this function has zero work to do for a rotate())
} else {
// operation = 4; // rotation
if (angle) {
@@ -778,19 +782,19 @@ export const recalculateDimensions = function (selected) {
const child = children.item(c);
if (child.tagName === 'tspan') {
const tspanChanges = {
x: $(child).attr('x') || 0,
y: $(child).attr('y') || 0
x: child.getAttribute('x') || 0,
y: child.getAttribute('y') || 0
};
remapElement(child, tspanChanges, m);
}
}
}
// [Rold][M][T][S][-T] became [Rold][M]
// we want it to be [Rnew][M][Tr] where Tr is the
// translation required to re-center it
// Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M]
// [Rold][M][T][S][-T] became [Rold][M]
// we want it to be [Rnew][M][Tr] where Tr is the
// translation required to re-center it
// Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M]
} else if (operation === 3 && angle) {
const {matrix} = transformListToTransform(tlist);
const { matrix } = transformListToTransform(tlist);
const roldt = svgroot.createSVGTransform();
roldt.setRotate(angle, oldcenter.x, oldcenter.y);
const rold = roldt.matrix;

View File

@@ -1,3 +1,4 @@
/* eslint-disable sonarjs/no-duplicate-string */
/**
* Tools for SVG sanitization.
* @module sanitize
@@ -8,7 +9,7 @@
import {getReverseNS, NS} from '../common/namespaces.js';
import {isGecko} from '../common/browser.js';
import {getHref, setHref, getUrlFromAttr} from '../common/utilities.js';
import {getHref, setHref, getUrlFromAttr} from './utilities.js';
const REVERSE_NS = getReverseNS();
@@ -21,38 +22,44 @@ const REVERSE_NS = getReverseNS();
/* eslint-disable max-len */
const svgWhiteList_ = {
// SVG Elements
a: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'xlink:href', 'xlink:title', 'display'],
circle: ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'r', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'display'],
a: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'xlink:href', 'xlink:title'],
circle: ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'r', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
clipPath: ['class', 'clipPathUnits', 'id'],
defs: [],
style: ['type'],
desc: [],
ellipse: ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'display'],
ellipse: ['class', 'clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
feBlend: ['in', 'in2'],
feComposite: ['operator', 'result', 'in2'],
feFlood: ['flood-color', 'in'],
feGaussianBlur: ['class', 'color-interpolation-filters', 'id', 'requiredFeatures', 'stdDeviation'],
feMerge: [],
feMergeNode: ['in'],
feMorphology: ['class', 'in', 'operator', 'radius'],
feOffset: ['dx', 'in', 'dy', 'result'],
filter: ['class', 'color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'id', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],
foreignObject: ['class', 'font-size', 'height', 'id', 'opacity', 'requiredFeatures', 'style', 'transform', 'width', 'x', 'y'],
g: ['class', 'clip-path', 'clip-rule', 'id', 'display', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'font-family', 'font-size', 'font-style', 'font-weight', 'text-anchor'],
image: ['class', 'clip-path', 'clip-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'style', 'systemLanguage', 'transform', 'width', 'x', 'xlink:href', 'xlink:title', 'y', 'display'],
line: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'x1', 'x2', 'y1', 'y2', 'display'],
image: ['class', 'clip-path', 'clip-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'style', 'systemLanguage', 'transform', 'width', 'x', 'xlink:href', 'xlink:title', 'y'],
line: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'x1', 'x2', 'y1', 'y2'],
linearGradient: ['class', 'id', 'gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2'],
marker: ['id', 'class', 'markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'systemLanguage', 'viewBox'],
mask: ['class', 'height', 'id', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y'],
metadata: ['class', 'id'],
path: ['class', 'clip-path', 'clip-rule', 'd', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'display'],
path: ['class', 'clip-path', 'clip-rule', 'd', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
pattern: ['class', 'height', 'id', 'patternContentUnits', 'patternTransform', 'patternUnits', 'requiredFeatures', 'style', 'systemLanguage', 'viewBox', 'width', 'x', 'xlink:href', 'y'],
polygon: ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'class', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'display'],
polyline: ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'display'],
polygon: ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'id', 'class', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
polyline: ['class', 'clip-path', 'clip-rule', 'id', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform'],
radialGradient: ['class', 'cx', 'cy', 'fx', 'fy', 'gradientTransform', 'gradientUnits', 'id', 'r', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'xlink:href'],
rect: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'width', 'x', 'y', 'display'],
rect: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'id', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'width', 'x', 'y'],
stop: ['class', 'id', 'offset', 'requiredFeatures', 'stop-color', 'stop-opacity', 'style', 'systemLanguage'],
svg: ['class', 'clip-path', 'clip-rule', 'filter', 'id', 'height', 'mask', 'preserveAspectRatio', 'requiredFeatures', 'style', 'systemLanguage', 'viewBox', 'width', 'x', 'xmlns', 'xmlns:se', 'xmlns:xlink', 'y'],
svg: ['class', 'clip-path', 'clip-rule', 'filter', 'id', 'height', 'mask', 'preserveAspectRatio', 'requiredFeatures', 'style', 'systemLanguage', 'viewBox', 'width', 'x', 'xmlns', 'xmlns:se', 'xmlns:xlink', 'xmlns:oi', 'oi:animations', 'y'],
switch: ['class', 'id', 'requiredFeatures', 'systemLanguage'],
symbol: ['class', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'opacity', 'preserveAspectRatio', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'transform', 'viewBox'],
text: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-decoration', 'letter-spacing', 'word-spacing', 'textLength', 'lengthAdjust', 'style', 'systemLanguage', 'text-anchor', 'transform', 'x', 'xml:space', 'y', 'display'],
textPath: ['class', 'id', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'style', 'systemLanguage', 'transform', 'xlink:href', 'display'],
text: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'text-anchor', 'transform', 'x', 'xml:space', 'y'],
textPath: ['class', 'id', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'style', 'systemLanguage', 'transform', 'xlink:href'],
title: [],
tspan: ['class', 'clip-path', 'clip-rule', 'dx', 'dy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'text-anchor', 'textLength', 'transform', 'x', 'xml:space', 'y', 'display'],
tspan: ['class', 'clip-path', 'clip-rule', 'dx', 'dy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'id', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'systemLanguage', 'text-anchor', 'textLength', 'transform', 'x', 'xml:space', 'y'],
use: ['class', 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'id', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'style', 'transform', 'width', 'x', 'xlink:href', 'y'],
// MathML Elements
@@ -94,7 +101,7 @@ const svgWhiteList_ = {
const svgWhiteListNS_ = {};
Object.entries(svgWhiteList_).forEach(function ([elt, atts]) {
const attNS = {};
Object.entries(atts).forEach(function ([i, att]) {
Object.entries(atts).forEach(function ([_i, att]) {
if (att.includes(':')) {
const v = att.split(':');
attNS[v[1]] = NS[(v[0]).toUpperCase()];
@@ -204,13 +211,10 @@ export const sanitizeSvg = function (node) {
const href = getHref(node);
if (href &&
['filter', 'linearGradient', 'pattern',
'radialGradient', 'textPath', 'use'].includes(node.nodeName)) {
// TODO: we simply check if the first character is a #, is this bullet-proof?
if (href[0] !== '#') {
// remove the attribute (but keep the element)
setHref(node, '');
node.removeAttributeNS(NS.XLINK, 'href');
}
'radialGradient', 'textPath', 'use'].includes(node.nodeName) && href[0] !== '#') {
// remove the attribute (but keep the element)
setHref(node, '');
node.removeAttributeNS(NS.XLINK, 'href');
}
// Safari crashes on a <use> without a xlink:href, so we just remove the node here

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* DOM element selection box tools.
* @module select
@@ -7,12 +6,10 @@
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import {isTouch, isWebkit} from '../common/browser.js'; // , isOpera
import {getRotationAngle, getBBox, getStrokedBBox, isNullish} from '../common/utilities.js';
import {transformListToTransform, transformBox, transformPoint} from '../common/math.js';
import {getTransformList} from '../common/svgtransformlist.js';
const $ = jQuery;
import { isTouch, isWebkit } from '../common/browser.js'; // , isOpera
import { getRotationAngle, getBBox, getStrokedBBox, isNullish } from './utilities.js';
import { transformListToTransform, transformBox, transformPoint } from './math.js';
import { getTransformList } from './svgtransformlist.js';
let svgFactory_;
let config_;
@@ -28,7 +25,7 @@ export class Selector {
* @param {Element} elem - DOM element associated with this selector
* @param {module:utilities.BBoxObject} [bbox] - Optional bbox to use for initialization (prevents duplicate `getBBox` call).
*/
constructor (id, elem, bbox) {
constructor(id, elem, bbox) {
// this is the selector's unique number
this.id = id;
@@ -41,24 +38,23 @@ export class Selector {
// this holds a reference to the <g> element that holds all visual elements of the selector
this.selectorGroup = svgFactory_.createSVGElement({
element: 'g',
attr: {id: ('selectorGroup' + this.id)}
attr: { id: ('selectorGroup' + this.id) }
});
// this holds a reference to the path rect
this.selectorRect = this.selectorGroup.appendChild(
svgFactory_.createSVGElement({
element: 'path',
attr: {
id: ('selectedBox' + this.id),
fill: 'none',
stroke: '#22C',
'stroke-width': '1',
'stroke-dasharray': '5,5',
// need to specify this so that the rect is not selectable
style: 'pointer-events:none'
}
})
);
this.selectorRect = svgFactory_.createSVGElement({
element: 'path',
attr: {
id: ('selectedBox' + this.id),
fill: 'none',
stroke: '#22C',
'stroke-width': '1',
'stroke-dasharray': '5,5',
// need to specify this so that the rect is not selectable
style: 'pointer-events:none'
}
});
this.selectorGroup.append(this.selectorRect);
// this holds a reference to the grip coordinates for this selector
this.gripCoords = {
@@ -81,7 +77,7 @@ export class Selector {
* @param {module:utilities.BBoxObject} bbox - Optional bbox to use for reset (prevents duplicate getBBox call).
* @returns {void}
*/
reset (e, bbox) {
reset(e, bbox) {
this.locked = true;
this.selectedElement = e;
this.resize(bbox);
@@ -93,7 +89,7 @@ export class Selector {
* @param {boolean} show - Indicates whether grips should be shown or not
* @returns {void}
*/
showGrips (show) {
showGrips(show) {
const bShow = show ? 'inline' : 'none';
selectorManager_.selectorGripsGroup.setAttribute('display', bShow);
const elem = this.selectedElement;
@@ -109,7 +105,8 @@ export class Selector {
* @param {module:utilities.BBoxObject} [bbox] - BBox to use for resize (prevents duplicate getBBox call).
* @returns {void}
*/
resize (bbox) {
resize(bbox) {
const dataStorage = svgFactory_.getDataStorage();
const selectedBox = this.selectorRect,
mgr = selectorManager_,
selectedGrips = mgr.selectorGrips,
@@ -121,7 +118,7 @@ export class Selector {
offset += (sw / 2);
}
const {tagName} = selected;
const { tagName } = selected;
if (tagName === 'text') {
offset += 2 / currentZoom;
}
@@ -140,7 +137,7 @@ export class Selector {
}
// TODO: getBBox (previous line) already knows to call getStrokedBBox when tagName === 'g'. Remove this?
// TODO: getBBox doesn't exclude 'gsvg' and calls getStrokedBBox for any 'g'. Should getBBox be updated?
if (tagName === 'g' && !$.data(selected, 'gsvg')) {
if (tagName === 'g' && !dataStorage.has(selected, 'gsvg')) {
// The bbox for a group does not include stroke vals, so we
// get the bbox based on its children.
const strokedBbox = getStrokedBBox([selected.childNodes]);
@@ -160,7 +157,7 @@ export class Selector {
offset *= currentZoom;
const nbox = transformBox(l * currentZoom, t * currentZoom, w * currentZoom, h * currentZoom, m),
{aabox} = nbox;
{ aabox } = nbox;
let nbax = aabox.x - offset,
nbay = aabox.y - offset,
nbaw = aabox.width + (offset * 2),
@@ -181,13 +178,13 @@ export class Selector {
nbox.br = transformPoint(nbox.br.x, nbox.br.y, rotm);
// calculate the axis-aligned bbox
const {tl} = nbox;
const { tl } = nbox;
let minx = tl.x,
miny = tl.y,
maxx = tl.x,
maxy = tl.y;
const {min, max} = Math;
const { min, max } = Math;
minx = min(minx, min(nbox.tr.x, min(nbox.bl.x, nbox.br.x))) - offset;
miny = min(miny, min(nbox.tr.y, min(nbox.bl.y, nbox.br.y))) - offset;
@@ -243,7 +240,7 @@ export class Selector {
* @param {Float} angle - Current rotation angle in degrees
* @returns {void}
*/
static updateGripCursors (angle) {
static updateGripCursors(angle) {
const dirArr = Object.keys(selectorManager_.selectorGrips);
let steps = Math.round(angle / 45);
if (steps < 0) { steps += 8; }
@@ -264,7 +261,7 @@ export class SelectorManager {
/**
* Sets up properties and calls `initGroup`.
*/
constructor () {
constructor() {
// this will hold the <g> element that contains all selector rects/grips
this.selectorParentGroup = null;
@@ -300,7 +297,8 @@ export class SelectorManager {
* Resets the parent selector group element.
* @returns {void}
*/
initGroup () {
initGroup() {
const dataStorage = svgFactory_.getDataStorage();
// remove old selector parent group if it existed
if (this.selectorParentGroup && this.selectorParentGroup.parentNode) {
this.selectorParentGroup.remove();
@@ -309,11 +307,11 @@ export class SelectorManager {
// create parent selector group and add it to svgroot
this.selectorParentGroup = svgFactory_.createSVGElement({
element: 'g',
attr: {id: 'selectorParentGroup'}
attr: { id: 'selectorParentGroup' }
});
this.selectorGripsGroup = svgFactory_.createSVGElement({
element: 'g',
attr: {display: 'none'}
attr: { display: 'none' }
});
this.selectorParentGroup.append(this.selectorGripsGroup);
svgFactory_.svgRoot().append(this.selectorParentGroup);
@@ -340,13 +338,14 @@ export class SelectorManager {
}
});
$.data(grip, 'dir', dir);
$.data(grip, 'type', 'resize');
this.selectorGrips[dir] = this.selectorGripsGroup.appendChild(grip);
dataStorage.put(grip, 'dir', dir);
dataStorage.put(grip, 'type', 'resize');
this.selectorGrips[dir] = grip;
this.selectorGripsGroup.append(grip);
});
// add rotator elems
this.rotateGripConnector = this.selectorGripsGroup.appendChild(
this.rotateGripConnector =
svgFactory_.createSVGElement({
element: 'line',
attr: {
@@ -354,10 +353,10 @@ export class SelectorManager {
stroke: '#22C',
'stroke-width': '1'
}
})
);
});
this.selectorGripsGroup.append(this.rotateGripConnector);
this.rotateGrip = this.selectorGripsGroup.appendChild(
this.rotateGrip =
svgFactory_.createSVGElement({
element: 'circle',
attr: {
@@ -366,13 +365,13 @@ export class SelectorManager {
r: gripRadius,
stroke: '#22C',
'stroke-width': 2,
style: 'cursor:url(' + config_.imgPath + 'rotate.png) 12 12, auto;'
style: 'cursor:url(' + config_.imgPath + 'rotate.svg) 12 12, auto;'
}
})
);
$.data(this.rotateGrip, 'type', 'rotate');
});
this.selectorGripsGroup.append(this.rotateGrip);
dataStorage.put(this.rotateGrip, 'type', 'rotate');
if ($('#canvasBackground').length) { return; }
if (document.getElementById('canvasBackground')) { return; }
const [width, height] = config_.dimensions;
const canvasbg = svgFactory_.createSVGElement({
@@ -416,7 +415,7 @@ export class SelectorManager {
* @param {module:utilities.BBoxObject} [bbox] - Optional bbox to use for reset (prevents duplicate getBBox call).
* @returns {Selector} The selector based on the given element
*/
requestSelector (elem, bbox) {
requestSelector(elem, bbox) {
if (isNullish(elem)) { return null; }
const N = this.selectors.length;
@@ -446,13 +445,13 @@ export class SelectorManager {
* @param {Element} elem - DOM element to remove the selector for
* @returns {void}
*/
releaseSelector (elem) {
releaseSelector(elem) {
if (isNullish(elem)) { return; }
const N = this.selectors.length,
sel = this.selectorMap[elem.id];
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
console.log('WARNING! selector was released but was already unlocked');
}
for (let i = 0; i < N; ++i) {
if (this.selectors[i] && this.selectors[i] === sel) {
@@ -464,7 +463,7 @@ export class SelectorManager {
// remove from DOM and store reference in JS but only if it exists in the DOM
try {
sel.selectorGroup.setAttribute('display', 'none');
} catch (e) {}
} catch (e) {/* empty fn */ }
break;
}
@@ -475,9 +474,9 @@ export class SelectorManager {
* @returns {SVGRectElement} The rubberBandBox DOM element. This is the rectangle drawn by
* the user for selecting/zooming
*/
getRubberBandBox () {
getRubberBandBox() {
if (!this.rubberBandBox) {
this.rubberBandBox = this.selectorParentGroup.appendChild(
this.rubberBandBox =
svgFactory_.createSVGElement({
element: 'rect',
attr: {
@@ -489,8 +488,8 @@ export class SelectorManager {
display: 'none',
style: 'pointer-events:none'
}
})
);
});
this.selectorParentGroup.append(this.rubberBandBox);
}
return this.rubberBandBox;
}

View File

@@ -6,26 +6,27 @@
*
* @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 jQueryPluginSVG from './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';
} from './utilities.js';
import {
transformPoint, matrixMultiply, transformListToTransform
} from '../common/math.js';
} from './math.js';
import {
getTransformList
} from '../common/svgtransformlist.js';
} from './svgtransformlist.js';
import {
recalculateDimensions
} from './recalculate.js';
import {
isGecko
} from '../common/browser.js'; // , supportsEditableText
import { getParents } from '../editor/components/jgraduate/Util.js';
const {
MoveElementCommand, BatchCommand, InsertElementCommand, RemoveElementCommand, ChangeElementCommand
@@ -53,10 +54,10 @@ export const init = function (elementContext) {
export const moveToTopSelectedElem = function () {
const [selected] = elementContext_.getSelectedElements();
if (!isNullish(selected)) {
let t = selected;
const t = selected;
const oldParent = t.parentNode;
const oldNextSibling = t.nextSibling;
t = t.parentNode.appendChild(t);
t.parentNode.append(t);
// If the element actually moved position, add the command and fire the changed
// event handler.
if (oldNextSibling !== t.nextSibling) {
@@ -79,7 +80,7 @@ export const moveToBottomSelectedElem = function () {
let t = selected;
const oldParent = t.parentNode;
const oldNextSibling = t.nextSibling;
let {firstChild} = t.parentNode;
let { firstChild } = t.parentNode;
if (firstChild.tagName === 'title') {
firstChild = firstChild.nextSibling;
}
@@ -115,17 +116,17 @@ export const moveUpDownSelected = function (dir) {
// curBBoxes = [];
let closest, foundCur;
// jQuery sorts this list
const list = $(elementContext_.getIntersectionList(getStrokedBBoxDefaultVisible([selected]))).toArray();
const list = elementContext_.getIntersectionList(getStrokedBBoxDefaultVisible([selected]));
if (dir === 'Down') { list.reverse(); }
$.each(list, function () {
Array.prototype.forEach.call(list, function (el) {
if (!foundCur) {
if (this === selected) {
if (el === selected) {
foundCur = true;
}
return true;
}
closest = this;
closest = el;
return false;
});
if (!closest) { return; }
@@ -133,7 +134,11 @@ export const moveUpDownSelected = function (dir) {
const t = selected;
const oldParent = t.parentNode;
const oldNextSibling = t.nextSibling;
$(closest)[dir === 'Down' ? 'before' : 'after'](t);
if (dir === 'Down') {
closest.insertAdjacentElement('beforebegin', t);
} else {
closest.insertAdjacentElement('afterend', t);
}
// If the element actually moved position, add the command and fire the changed
// event handler.
if (oldNextSibling !== t.nextSibling) {
@@ -231,14 +236,24 @@ export const cloneSelectedElements = function (x, y) {
const batchCmd = new BatchCommand('Clone Elements');
// find all the elements selected (stop at first null)
const len = selectedElements.length;
function index(el) {
if (!el) return -1;
var i = 0;
do {
i++;
} while (el == el.previousElementSibling);
return i;
}
/**
* Sorts an array numerically and ascending.
* @param {Element} a
* @param {Element} b
* @returns {Integer}
*/
function sortfunction (a, b) {
return ($(b).index() - $(a).index());
function sortfunction(a, b) {
return (index(b) - index(a));
}
selectedElements.sort(sortfunction);
for (i = 0; i < len; ++i) {
@@ -287,40 +302,40 @@ export const alignSelectedElements = function (type, relativeTo) {
// 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;
case 'smallest':
if (((type === 'l' || type === 'c' || type === 'r' || type === 'left' || type === 'center' || type === 'right') &&
(curwidth === Number.MIN_VALUE || curwidth > bboxes[i].width)) ||
((type === 't' || type === 'm' || type === 'b' || type === 'top' || type === 'middle' || type === 'bottom') &&
(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' || type === 'left' || type === 'center' || type === 'right') &&
(curwidth === Number.MIN_VALUE || curwidth < bboxes[i].width)) ||
((type === 't' || type === 'm' || type === 'b' || type === 'top' || type === 'middle' || type === 'bottom') &&
(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
@@ -340,24 +355,30 @@ export const alignSelectedElements = function (type, relativeTo) {
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;
case 'l': // left (horizontal)
case 'left': // left (horizontal)
dx[i] = minx - bbox.x;
break;
case 'c': // center (horizontal)
case 'center': // center (horizontal)
dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2);
break;
case 'r': // right (horizontal)
case 'right': // right (horizontal)
dx[i] = maxx - (bbox.x + bbox.width);
break;
case 't': // top (vertical)
case 'top': // top (vertical)
dy[i] = miny - bbox.y;
break;
case 'm': // middle (vertical)
case 'middle': // middle (vertical)
dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2);
break;
case 'b': // bottom (vertical)
case 'bottom': // bottom (vertical)
dy[i] = maxy - (bbox.y + bbox.height);
break;
}
}
moveSelectedElements(dx, dy);
@@ -395,7 +416,7 @@ export const deleteSelectedElements = function () {
parent = parent.parentNode;
}
const {nextSibling} = t;
const { nextSibling } = t;
t.remove();
const elem = t;
selectedCopy.push(selected); // for the copy
@@ -416,16 +437,14 @@ export const deleteSelectedElements = function () {
export const copySelectedElements = function () {
const selectedElements = elementContext_.getSelectedElements();
const data =
JSON.stringify(selectedElements.map((x) => elementContext_.getJsonFromSvgElement(x)));
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');
}
const canvMenu = document.getElementById('se-cmenu_canvas');
canvMenu.setAttribute('enablemenuitems', '#paste,#paste_in_place');
};
/**
@@ -441,16 +460,17 @@ export const groupSelectedElements = function (type, urlArg) {
let cmdStr = '';
let url;
// eslint-disable-next-line sonarjs/no-small-switch
switch (type) {
case 'a': {
cmdStr = 'Make hyperlink';
url = urlArg || '';
break;
} default: {
type = 'g';
cmdStr = 'Group Elements';
break;
}
case 'a': {
cmdStr = 'Make hyperlink';
url = urlArg || '';
break;
} default: {
type = 'g';
cmdStr = 'Group Elements';
break;
}
}
const batchCmd = new BatchCommand(cmdStr);
@@ -515,7 +535,10 @@ export const pushGroupProperty = function (g, undoable) {
const gangle = getRotationAngle(g);
const gattrs = $(g).attr(['filter', 'opacity']);
const gattrs = {
filter: g.getAttribute('filter'),
opacity: g.getAttribute('opacity'),
};
let gfilter, gblur, changes;
const drawing = elementContext_.getDrawing();
@@ -688,17 +711,20 @@ export const convertToGroup = function (elem) {
if (!elem) {
elem = selectedElements[0];
}
const $elem = $(elem);
const $elem = elem;
const batchCmd = new BatchCommand();
let ts;
if ($elem.data('gsvg')) {
const dataStorage = elementContext_.getDataStorage();
if (dataStorage.has($elem, 'gsvg')) {
// Use the gsvg as the new group
const svg = elem.firstChild;
const pt = $(svg).attr(['x', 'y']);
const pt = {
x: svg.getAttribute('x'),
y: svg.getAttribute('y'),
};
$(elem.firstChild.firstChild).unwrap();
$(elem).removeData('gsvg');
dataStorage.remove(elem, 'gsvg');
const tlist = getTransformList(elem);
const xform = elementContext_.getSVGRoot().createSVGTransform();
@@ -706,8 +732,8 @@ export const convertToGroup = function (elem) {
tlist.appendItem(xform);
recalculateDimensions(elem);
elementContext_.call('selected', [elem]);
} else if ($elem.data('symbol')) {
elem = $elem.data('symbol');
} else if (dataStorage.has($elem, 'symbol')) {
elem = dataStorage.get($elem, 'symbol')
ts = $elem.attr('transform');
const pos = $elem.attr(['x', 'y']);
@@ -731,7 +757,7 @@ export const convertToGroup = function (elem) {
// See if other elements reference this symbol
const svgcontent = elementContext_.getSVGContent();
const hasMore = $(svgcontent).find('use:data(symbol)').length;
const hasMore = svgcontent.querySelectorAll('use:data(symbol)').length;
const g = elementContext_.getDOMDocument().createElementNS(NS.SVG, 'g');
const childs = elem.childNodes;
@@ -743,8 +769,11 @@ export const convertToGroup = function (elem) {
// 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);
const svgElement = findDefs();
const gradients = svgElement.querySelectorAll('linearGradient,radialGradient,pattern');
for (let i = 0, im = gradients.length; im > i; i++) {
g.appendChild(gradients[i].cloneNode(true));
}
}
if (ts) {
@@ -757,7 +786,11 @@ export const convertToGroup = function (elem) {
// Put the dupe gradients back into <defs> (after uniquifying them)
if (isGecko()) {
$(findDefs()).append($(g).find('linearGradient,radialGradient,pattern'));
const svgElement = findDefs();
const elements = g.querySelectorAll('linearGradient,radialGradient,pattern');
for (let i = 0, im = elements.length; im > i; i++) {
svgElement.appendChild(elements[i]);
}
}
// now give the g itself a new id
@@ -768,7 +801,7 @@ export const convertToGroup = function (elem) {
if (parent) {
if (!hasMore) {
// remove symbol/svg element
const {nextSibling} = elem;
const { nextSibling } = elem;
elem.remove();
batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));
}
@@ -789,14 +822,14 @@ export const convertToGroup = function (elem) {
try {
recalculateDimensions(n);
} catch (e) {
console.log(e); // eslint-disable-line no-console
console.log(e);
}
});
// Give ID for any visible element missing one
const visElems = elementContext_.getVisElems();
$(g).find(visElems).each(function () {
if (!this.id) { this.id = elementContext_.getNextId(); }
const visElems = g.querySelectorAll(elementContext_.getVisElems());
Array.prototype.forEach.call(visElems, function (el) {
if (!el.id) { el.id = elementContext_.getNextId(); }
});
elementContext_.selectOnly([g]);
@@ -808,7 +841,7 @@ export const convertToGroup = function (elem) {
elementContext_.addCommandToHistory(batchCmd);
} else {
console.log('Unexpected element to ungroup:', elem); // eslint-disable-line no-console
console.log('Unexpected element to ungroup:', elem);
}
};
@@ -820,11 +853,12 @@ export const convertToGroup = function (elem) {
*/
export const ungroupSelectedElement = function () {
const selectedElements = elementContext_.getSelectedElements();
const dataStorage = elementContext_.getDataStorage();
let g = selectedElements[0];
if (!g) {
return;
}
if ($(g).data('gsvg') || $(g).data('symbol')) {
if (dataStorage.has(g, 'gsvg') || dataStorage.has(g, 'symbol')) {
// Is svg, so actually convert to group
convertToGroup(g);
return;
@@ -832,11 +866,12 @@ export const ungroupSelectedElement = function () {
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);
dataStorage.put(g, 'symbol', symbol);
dataStorage.put(g, 'ref', symbol);
convertToGroup(g);
return;
}
const parentsA = $(g).parents('a');
const parentsA = getParents(g.parentNode, 'a');
if (parentsA.length) {
g = parentsA[0];
}
@@ -859,7 +894,7 @@ export const ungroupSelectedElement = function () {
// Remove child title elements
if (elem.tagName === 'title') {
const {nextSibling} = elem;
const { nextSibling } = elem;
batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, oldParent));
elem.remove();
continue;
@@ -900,7 +935,7 @@ 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 bg = document.getElementById('canvasBackground');
const oldX = elementContext_.getSVGContent().getAttribute('x');
const oldY = elementContext_.getSVGContent().getAttribute('y');
const x = ((w - this.contentW * currentZoom) / 2);
@@ -947,9 +982,9 @@ export const updateCanvas = function (w, h) {
/**
* @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}
{ 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};
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.

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* Tools for selection.
* @module selection
@@ -6,19 +5,18 @@
* @copyright 2011 Jeff Schiller
*/
import {NS} from '../common/namespaces.js';
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';
} from './utilities.js';
import { transformPoint, transformListToTransform, rectsIntersect } from './math.js';
import {
getTransformList
} from '../common/svgtransformlist.js';
} from './svgtransformlist.js';
import * as hstry from './history.js';
import { getClosest } from '../editor/components/jgraduate/Util.js';
const {BatchCommand} = hstry;
const $ = jQueryPluginSVG(jQuery);
const { BatchCommand } = hstry;
let selectionContext_ = null;
/**
@@ -139,7 +137,7 @@ export const getMouseTargetMethod = function (evt) {
// 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'
mouseTarget.id !== 'svgcanvas'
) {
while (mouseTarget.nodeName !== 'foreignObject') {
mouseTarget = mouseTarget.parentNode;
@@ -157,10 +155,10 @@ mouseTarget.id !== 'svgcanvas'
return selectionContext_.getSVGRoot();
}
const $target = $(mouseTarget);
const $target = mouseTarget;
// If it's a selection grip, return the grip parent
if ($target.closest('#selectorParentGroup').length) {
if (getClosest($target.parentNode, '#selectorParentGroup')) {
// While we could instead have just returned mouseTarget,
// this makes it easier to indentify as being a selector grip
return selectionContext_.getCanvas().selectorManager.selectorParentGroup;
@@ -209,7 +207,7 @@ mouseTarget.id !== 'svgcanvas'
*/
export const runExtensionsMethod = function (action, vars, returnArray, nameFilter) {
let result = returnArray ? [] : false;
$.each(selectionContext_.getExtensions(), function (name, ext) {
for (const [name, ext] of Object.entries(selectionContext_.getExtensions())) {
if (nameFilter && !nameFilter(name)) {
return;
}
@@ -223,7 +221,7 @@ export const runExtensionsMethod = function (action, vars, returnArray, nameFilt
result = ext[action](vars);
}
}
});
}
return result;
};
@@ -237,12 +235,14 @@ export const runExtensionsMethod = function (action, vars, returnArray, nameFilt
*/
export const getVisibleElementsAndBBoxes = function (parent) {
if (!parent) {
parent = $(selectionContext_.getSVGContent()).children(); // Prevent layers from being included
const svgcontent = selectionContext_.getSVGContent();
parent = svgcontent.children; // Prevent layers from being included
}
const contentElems = [];
$(parent).children().each(function (i, elem) {
const elements = parent.children;
Array.prototype.forEach.call(elements, function (elem) {
if (elem.getBBox) {
contentElems.push({elem, bbox: getStrokedBBoxDefaultVisible([elem])});
contentElems.push({ elem, bbox: getStrokedBBoxDefaultVisible([elem]) });
}
});
return contentElems.reverse();
@@ -284,15 +284,6 @@ export const getIntersectionListMethod = function (rect) {
}
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 = [];
@@ -330,9 +321,12 @@ export const getIntersectionListMethod = function (rect) {
* @returns {void}
*/
export const groupSvgElem = function (elem) {
const dataStorage = selectionContext_.getDataStorage();
const g = document.createElementNS(NS.SVG, 'g');
elem.replaceWith(g);
$(g).append(elem).data('gsvg', elem)[0].id = selectionContext_.getCanvas().getNextId();
g.appendChild(elem);
dataStorage.put(g, 'gsvg', elem);
g.id = selectionContext_.getCanvas().getNextId();
};
/**
@@ -351,12 +345,6 @@ export const prepareSvg = function (newDoc) {
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
@@ -402,7 +390,11 @@ export const setRotationAngle = function (val, 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);
if (oldTransform) {
elem.setAttribute('transform', oldTransform);
} else {
elem.removeAttribute('transform');
}
selectionContext_.getCanvas().changeSelectedAttribute('transform', newTransform, selectedElements);
selectionContext_.getCanvas().call('changed', selectedElements);
}

View File

@@ -6,29 +6,30 @@
* @copyright 2011 Jeff Schiller
*/
import {jsPDF} from 'jspdf/dist/jspdf.es.min.js';
import { jsPDF } from 'jspdf/dist/jspdf.es.min.js';
import 'svg2pdf.js/dist/svg2pdf.es.js';
import jQueryPluginSVG from '../common/jQuery.attr.js';
import jQueryPluginSVG from './jQuery.attr.js';
import * as hstry from './history.js';
import {
text2xml, cleanupElement, findDefs, getHref, preventClickDefault,
toXml, getStrokedBBoxDefaultVisible, encode64, createObjectURL,
dataURLToObjectURL, walkTree, getBBox as utilsGetBBox
} from '../common/utilities.js';
} from './utilities.js';
import {
transformPoint, transformListToTransform
} from '../common/math.js';
import {resetListMap} from '../common/svgtransformlist.js';
} from './math.js';
import { resetListMap } from './svgtransformlist.js';
import {
convertUnit, shortFloat, convertToNum
} from '../common/units.js';
import {isGecko, isChrome, isWebkit} from '../common/browser.js';
import { isGecko, isChrome, isWebkit } from '../common/browser.js';
import * as pathModule from './path.js';
import {NS} from '../common/namespaces.js';
import { NS } from '../common/namespaces.js';
import * as draw from './draw.js';
import {
recalculateDimensions
} from './recalculate.js';
import { getParents, getClosest } from '../editor/components/jgraduate/Util.js';
const {
InsertElementCommand, RemoveElementCommand,
@@ -38,6 +39,7 @@ const {
const $ = jQueryPluginSVG(jQuery);
let svgContext_ = null;
let $id = null;
/**
* @function module:svg-exec.init
@@ -46,6 +48,8 @@ let svgContext_ = null;
*/
export const init = function (svgContext) {
svgContext_ = svgContext;
const svgCanvas = svgContext_.getCanvas();
$id = svgCanvas.$id;
};
/**
@@ -55,12 +59,13 @@ export const init = function (svgContext) {
*/
export const svgCanvasToString = function () {
// keep calling it until there are none to remove
while (svgContext_.getCanvas().removeUnusedDefElems() > 0) {} // eslint-disable-line no-empty
while (svgContext_.getCanvas().removeUnusedDefElems() > 0) { } // eslint-disable-line no-empty
svgContext_.getCanvas().pathActions.clear(true);
// Keep SVG-Edit comment on top
$.each(svgContext_.getSVGContent().childNodes, function (i, node) {
const childNodesElems = svgContext_.getSVGContent().childNodes;
childNodesElems.forEach(function (node, i) {
if (i && node.nodeType === 8 && node.data.includes('Created with')) {
svgContext_.getSVGContent().firstChild.before(node);
}
@@ -75,8 +80,9 @@ export const svgCanvasToString = function () {
const nakedSvgs = [];
// Unwrap gsvg if it has no special attributes (only id and style)
$(svgContext_.getSVGContent()).find('g:data(gsvg)').each(function () {
const attrs = this.attributes;
const gsvgElems = svgContext_.getSVGContent().querySelectorAll('g[data-gsvg]');
Array.prototype.forEach.call(gsvgElems, function (element) {
const attrs = element.attributes;
let len = attrs.length;
for (let i = 0; i < len; i++) {
if (attrs[i].nodeName === 'id' || attrs[i].nodeName === 'style') {
@@ -85,17 +91,17 @@ export const svgCanvasToString = function () {
}
// No significant attributes, so ungroup
if (len <= 0) {
const svg = this.firstChild;
const svg = element.firstChild;
nakedSvgs.push(svg);
$(this).replaceWith(svg);
element.replaceWith(svg);
}
});
const output = this.svgToString(svgContext_.getSVGContent(), 0);
// Rewrap gsvg
if (nakedSvgs.length) {
$(nakedSvgs).each(function () {
svgContext_.getCanvas().groupSvgElem(this);
Array.prototype.forEach.call(nakedSvgs, function (el) {
svgContext_.getCanvas().groupSvgElem(el);
});
}
@@ -153,22 +159,26 @@ export const svgToString = function (elem, indent) {
const nsuris = {};
// Check elements for namespaces, add if found
$(elem).find('*').andSelf().each(function () {
const csElements = elem.querySelectorAll('*');
const cElements = Array.prototype.slice.call(csElements);
cElements.push(elem);
Array.prototype.forEach.call(cElements, function (el) {
// const el = this;
// for some elements have no attribute
const uri = this.namespaceURI;
const uri = el.namespaceURI;
if (uri && !nsuris[uri] && nsMap[uri] && nsMap[uri] !== 'xmlns' && nsMap[uri] !== 'xml') {
nsuris[uri] = true;
out.push(' xmlns:' + nsMap[uri] + '="' + uri + '"');
}
$.each(this.attributes, function (i, attr) {
const u = attr.namespaceURI;
if (u && !nsuris[u] && nsMap[u] !== 'xmlns' && nsMap[u] !== 'xml') {
nsuris[u] = true;
out.push(' xmlns:' + nsMap[u] + '="' + u + '"');
if (el.attributes.length > 0) {
for (const [, attr] of Object.entries(el.attributes)) {
const u = attr.namespaceURI;
if (u && !nsuris[u] && nsMap[u] !== 'xmlns' && nsMap[u] !== 'xml') {
nsuris[u] = true;
out.push(' xmlns:' + nsMap[u] + '="' + u + '"');
}
}
});
}
});
let i = attrs.length;
@@ -181,12 +191,10 @@ export const svgToString = function (elem, indent) {
if (attr.nodeName.startsWith('xmlns:')) { continue; }
// only serialize attributes we don't use internally
if (attrVal !== '' && !attrNames.includes(attr.localName)) {
if (!attr.namespaceURI || nsMap[attr.namespaceURI]) {
out.push(' ');
out.push(attr.nodeName); out.push('="');
out.push(attrVal); out.push('"');
}
if (attrVal !== '' && !attrNames.includes(attr.localName) && (!attr.namespaceURI || nsMap[attr.namespaceURI])) {
out.push(' ');
out.push(attr.nodeName); out.push('="');
out.push(attrVal); out.push('"');
}
}
} else {
@@ -216,10 +224,10 @@ export const svgToString = function (elem, indent) {
// Embed images when saving
if (svgContext_.getSvgOptionApply() &&
elem.nodeName === 'image' &&
attr.localName === 'href' &&
svgContext_.getSvgOptionImages() &&
svgContext_.getSvgOptionImages() === 'embed'
elem.nodeName === 'image' &&
attr.localName === 'href' &&
svgContext_.getSvgOptionImages() &&
svgContext_.getSvgOptionImages() === 'embed'
) {
const img = svgContext_.getEncodableImages(attrVal);
if (img) { attrVal = img; }
@@ -243,31 +251,31 @@ export const svgToString = function (elem, indent) {
for (let i = 0; i < childs.length; i++) {
const child = childs.item(i);
switch (child.nodeType) {
case 1: // element node
out.push('\n');
out.push(this.svgToString(child, indent));
break;
case 3: { // text node
const str = child.nodeValue.replace(/^\s+|\s+$/g, '');
if (str !== '') {
bOneLine = true;
out.push(String(toXml(str)));
}
break;
} case 4: // cdata node
out.push('\n');
out.push(new Array(indent + 1).join(' '));
out.push('<![CDATA[');
out.push(child.nodeValue);
out.push(']]>');
break;
case 8: // comment
out.push('\n');
out.push(new Array(indent + 1).join(' '));
out.push('<!--');
out.push(child.data);
out.push('-->');
break;
case 1: // element node
out.push('\n');
out.push(this.svgToString(child, indent));
break;
case 3: { // text node
const str = child.nodeValue.replace(/^\s+|\s+$/g, '');
if (str !== '') {
bOneLine = true;
out.push(String(toXml(str)));
}
break;
} case 4: // cdata node
out.push('\n');
out.push(new Array(indent + 1).join(' '));
out.push('<![CDATA[');
out.push(child.nodeValue);
out.push(']]>');
break;
case 8: // comment
out.push('\n');
out.push(new Array(indent + 1).join(' '));
out.push('<!--');
out.push(child.data);
out.push('-->');
break;
} // switch on node type
}
indent--;
@@ -298,11 +306,12 @@ export const svgToString = function (elem, indent) {
*/
export const setSvgString = function (xmlString, preventUndo) {
const curConfig = svgContext_.getCurConfig();
const dataStorage = svgContext_.getDataStorage();
try {
// convert string into XML document
const newDoc = text2xml(xmlString);
if (newDoc.firstElementChild &&
newDoc.firstElementChild.namespaceURI !== NS.SVG) {
newDoc.firstElementChild.namespaceURI !== NS.SVG) {
return false;
}
@@ -311,7 +320,7 @@ export const setSvgString = function (xmlString, preventUndo) {
const batchCmd = new BatchCommand('Change Source');
// remove old svg document
const {nextSibling} = svgContext_.getSVGContent();
const { nextSibling } = svgContext_.getSVGContent();
svgContext_.getSVGContent().remove();
const oldzoom = svgContext_.getSVGContent();
@@ -326,7 +335,7 @@ export const setSvgString = function (xmlString, preventUndo) {
}
svgContext_.getSVGRoot().append(svgContext_.getSVGContent());
const content = $(svgContext_.getSVGContent());
const content = svgContext_.getSVGContent();
svgContext_.getCanvas().current_drawing_ = new draw.Drawing(svgContext_.getSVGContent(), svgContext_.getIdPrefix());
@@ -339,8 +348,8 @@ export const setSvgString = function (xmlString, preventUndo) {
}
// change image href vals if possible
content.find('image').each(function () {
const image = this;
const elements = content.querySelectorAll('image');
Array.prototype.forEach.call(elements, function (image) {
preventClickDefault(image);
const val = svgContext_.getCanvas().getHref(this);
if (val) {
@@ -351,9 +360,11 @@ export const setSvgString = function (xmlString, preventUndo) {
if (m) {
const url = decodeURIComponent(m[1]);
// const url = decodeURIComponent(m.groups.url);
$(new Image()).load(function () {
const iimg = new Image();
iimg.addEventListener("load", () => {
image.setAttributeNS(NS.XLINK, 'xlink:href', url);
}).attr('src', url);
});
iimg.src = url;
}
}
// Add to encodableImages if it loads
@@ -362,25 +373,30 @@ export const setSvgString = function (xmlString, preventUndo) {
});
// Wrap child SVGs in group elements
content.find('svg').each(function () {
const svgElements = content.querySelectorAll('svg');
Array.prototype.forEach.call(svgElements, function (element) {
// Skip if it's in a <defs>
if ($(this).closest('defs').length) { return; }
if (getClosest(element.parentNode, 'defs')) { return; }
svgContext_.getCanvas().uniquifyElems(this);
svgContext_.getCanvas().uniquifyElems(element);
// Check if it already has a gsvg group
const pa = this.parentNode;
const pa = element.parentNode;
if (pa.childNodes.length === 1 && pa.nodeName === 'g') {
$(pa).data('gsvg', this);
dataStorage.put(pa, 'gsvg', element);
pa.id = pa.id || svgContext_.getCanvas().getNextId();
} else {
svgContext_.getCanvas().groupSvgElem(this);
svgContext_.getCanvas().groupSvgElem(element);
}
});
// For Firefox: Put all paint elems in defs
if (isGecko()) {
content.find('linearGradient, radialGradient, pattern').appendTo(findDefs());
const svgDefs = findDefs();
const findElems = content.querySelectorAll('linearGradient, radialGradient, pattern');
Array.prototype.forEach.call(findElems, function (ele) {
svgDefs.appendChild(ele);
});
}
// Set ref element for <use> elements
@@ -388,7 +404,7 @@ export const setSvgString = function (xmlString, preventUndo) {
// TODO: This should also be done if the object is re-added through "redo"
svgContext_.getCanvas().setUseData(content);
svgContext_.getCanvas().convertGradients(content[0]);
svgContext_.getCanvas().convertGradients(content);
const attrs = {
id: 'svgcontent',
@@ -398,16 +414,16 @@ export const setSvgString = function (xmlString, preventUndo) {
let percs = false;
// determine proper size
if (content.attr('viewBox')) {
const vb = content.attr('viewBox').split(' ');
if (content.getAttribute('viewBox')) {
const viBox = content.getAttribute('viewBox');
const vb = viBox.split(' ');
attrs.width = vb[2];
attrs.height = vb[3];
// handle content that doesn't have a viewBox
} else {
$.each(['width', 'height'], function (i, dim) {
['width', 'height'].forEach(function (dim) {
// Set to 100 if not given
const val = content.attr(dim) || '100%';
const val = content.getAttribute(dim) || '100%';
if (String(val).substr(-1) === '%') {
// Use user units if percentage given
percs = true;
@@ -421,8 +437,12 @@ export const setSvgString = function (xmlString, preventUndo) {
draw.identifyLayers();
// Give ID for any visible layer children missing one
content.children().find(svgContext_.getVisElems()).each(function () {
if (!this.id) { this.id = svgContext_.getCanvas().getNextId(); }
const chiElems = content.children;
Array.prototype.forEach.call(chiElems, function (chiElem) {
const visElems = chiElem.querySelectorAll(svgContext_.getVisElems());
Array.prototype.forEach.call(visElems, function (elem) {
if (!elem.id) { elem.id = svgContext_.getCanvas().getNextId(); }
});
});
// Percentage width/height, so let's base it on visible elements
@@ -437,13 +457,17 @@ export const setSvgString = function (xmlString, preventUndo) {
if (attrs.width <= 0) { attrs.width = 100; }
if (attrs.height <= 0) { attrs.height = 100; }
content.attr(attrs);
for (const [key, value] of Object.entries(attrs)) {
content.setAttribute(key, value);
}
this.contentW = attrs.width;
this.contentH = attrs.height;
batchCmd.addSubCommand(new InsertElementCommand(svgContext_.getSVGContent()));
// update root to the correct size
const changes = content.attr(['width', 'height']);
const width = content.getAttribute('width');
const height = content.getAttribute('height');
const changes = { width: width, height: height };
batchCmd.addSubCommand(new ChangeElementCommand(svgContext_.getSVGRoot(), changes));
// reset zoom
@@ -458,7 +482,7 @@ export const setSvgString = function (xmlString, preventUndo) {
if (!preventUndo) svgContext_.addCommandToHistory(batchCmd);
svgContext_.call('changed', [svgContext_.getSVGContent()]);
} catch (e) {
console.log(e); // eslint-disable-line no-console
console.log(e);
return false;
}
@@ -480,6 +504,7 @@ export const setSvgString = function (xmlString, preventUndo) {
* was obtained
*/
export const importSvgString = function (xmlString) {
const dataStorage = svgContext_.getDataStorage();
let j, ts, useEl;
try {
// Get unique ID
@@ -487,8 +512,9 @@ export const importSvgString = function (xmlString) {
let useExisting = false;
// Look for symbol and make sure symbol exists in image
if (svgContext_.getImportIds(uid)) {
if ($(svgContext_.getImportIds(uid).symbol).parents('#svgroot').length) {
if (svgContext_.getImportIds(uid) && svgContext_.getImportIds(uid).symbol) {
const parents = getParents(svgContext_.getImportIds(uid).symbol, '#svgroot');
if (parents.length) {
useExisting = true;
}
}
@@ -496,7 +522,7 @@ export const importSvgString = function (xmlString) {
const batchCmd = new BatchCommand('Import Image');
let symbol;
if (useExisting) {
({symbol} = svgContext_.getImportIds());
({ symbol } = svgContext_.getImportIds());
ts = svgContext_.getImportIds(uid).xform;
} else {
// convert string into XML document
@@ -538,7 +564,10 @@ export const importSvgString = function (xmlString) {
// Move all gradients into root for Firefox, workaround for this bug:
// https://bugzilla.mozilla.org/show_bug.cgi?id=353575
// TODO: Make this properly undo-able.
$(svg).find('linearGradient, radialGradient, pattern').appendTo(defs);
const elements = svg.querySelectorAll('linearGradient, radialGradient, pattern');
Array.prototype.forEach.call(elements, function (el) {
defs.appendChild(el);
});
}
while (svg.firstChild) {
@@ -571,7 +600,8 @@ export const importSvgString = function (xmlString) {
useEl.setAttribute('transform', ts);
recalculateDimensions(useEl);
$(useEl).data('symbol', symbol).data('ref', symbol);
dataStorage.put(useEl, 'symbol', symbol);
dataStorage.put(useEl, 'ref', symbol);
svgContext_.getCanvas().addToSelection([useEl]);
// TODO: Find way to add this in a recalculateDimensions-parsable way
@@ -581,7 +611,7 @@ export const importSvgString = function (xmlString) {
svgContext_.addCommandToHistory(batchCmd);
svgContext_.call('changed', [svgContext_.getSVGContent()]);
} catch (e) {
console.log(e); // eslint-disable-line no-console
console.log(e);
return null;
}
@@ -602,20 +632,16 @@ export const importSvgString = function (xmlString) {
*/
export const embedImage = function (src) {
// Todo: Remove this Promise in favor of making an async/await `Image.load` utility
// eslint-disable-next-line promise/avoid-new
return new Promise(function (resolve, reject) {
// load in the image and once it's loaded, get the dimensions
$(new Image()).load(function (response, status, xhr) {
if (status === 'error') {
reject(new Error('Error loading image: ' + xhr.status + ' ' + xhr.statusText));
return;
}
const imgI = new Image();
imgI.addEventListener("load", (e) => {
// create a canvas the same size as the raster image
const cvs = document.createElement('canvas');
cvs.width = this.width;
cvs.height = this.height;
cvs.width = e.currentTarget.width;
cvs.height = e.currentTarget.height;
// load the raster image into the canvas
cvs.getContext('2d').drawImage(this, 0, 0);
cvs.getContext('2d').drawImage(e.currentTarget, 0, 0);
// retrieve the data: URL
try {
let urldata = ';svgedit_url=' + encodeURIComponent(src);
@@ -626,7 +652,11 @@ export const embedImage = function (src) {
}
svgContext_.getCanvas().setGoodImage(src);
resolve(svgContext_.getEncodableImages(src));
}).attr('src', src);
});
imgI.addEventListener("error", () => {
reject(new Error('Error loading image: '));
});
imgI.setAttribute('src', src);
});
};
@@ -660,7 +690,7 @@ export const save = function (opts) {
* Codes only is useful for locale-independent detection.
* @returns {module:svgcanvas.IssuesAndCodes}
*/
function getIssues () {
function getIssues() {
const uiStrings = svgContext_.getUIStrings();
// remove the selected outline before serializing
svgContext_.getCanvas().clearSelection();
@@ -675,20 +705,20 @@ function getIssues () {
foreignObject: uiStrings.exportNoforeignObject,
'[stroke-dasharray]': uiStrings.exportNoDashArray
};
const content = $(svgContext_.getSVGContent());
const content = svgContext_.getSVGContent();
// Add font/text check if Canvas Text API is not implemented
if (!('font' in $('<canvas>')[0].getContext('2d'))) {
if (!('font' in document.querySelector('CANVAS').getContext('2d'))) {
issueList.text = uiStrings.exportNoText;
}
$.each(issueList, function (sel, descr) {
if (content.find(sel).length) {
for (const [sel, descr] of Object.entries(issueList)) {
if (content.querySelectorAll(sel).length) {
issueCodes.push(sel);
issues.push(descr);
}
});
return {issues, issueCodes};
}
return { issues, issueCodes };
}
/**
* @typedef {PlainObject} module:svgcanvas.ImageExportedResults
@@ -720,20 +750,25 @@ function getIssues () {
export const rasterExport = async function (imgType, quality, exportWindowName, opts = {}) {
const type = imgType === 'ICO' ? 'BMP' : (imgType || 'PNG');
const mimeType = 'image/' + type.toLowerCase();
const {issues, issueCodes} = getIssues();
const { issues, issueCodes } = getIssues();
const svg = this.svgCanvasToString();
if (!$('#export_canvas').length) {
$('<canvas>', {id: 'export_canvas'}).hide().appendTo('body');
if (!$id('export_canvas')) {
const canvasEx = document.createElement('CANVAS');
canvasEx.id = 'export_canvas';
canvasEx.style.display = 'none';
document.body.appendChild(canvasEx);
}
const c = $('#export_canvas')[0];
c.width = svgContext_.getCanvas().contentW;
c.height = svgContext_.getCanvas().contentH;
const c = $id('export_canvas');
c.style.width = svgContext_.getCanvas().contentW + "px";
c.style.height = svgContext_.getCanvas().contentH + "px";
const canvg = svgContext_.getcanvg();
await canvg(c, svg);
const ctx = c.getContext('2d');
const v = canvg.fromString(ctx, svg);
// Render only first frame, ignoring animations.
await v.render();
// Todo: Make async/await utility in place of `toBlob`, so we can remove this constructor
// eslint-disable-next-line promise/avoid-new
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
const dataURLType = type.toLowerCase();
const datauri = quality
? c.toDataURL('image/' + dataURLType, quality)
@@ -743,7 +778,7 @@ export const rasterExport = async function (imgType, quality, exportWindowName,
* Called when `bloburl` is available for export.
* @returns {void}
*/
function done () {
function done() {
const obj = {
datauri, bloburl, svg, issues, issueCodes, type: imgType,
mimeType, quality, exportWindowName
@@ -820,17 +855,17 @@ export const exportPDF = async (
keywords: '',
creator: '' */
});
const {issues, issueCodes} = getIssues();
const { issues, issueCodes } = getIssues();
// const svg = this.svgCanvasToString();
// await doc.addSvgAsImage(svg)
await doc.svg(svgContext_.getSVGContent(), {x: 0, y: 0, width: res.w, height: res.h});
await doc.svg(svgContext_.getSVGContent(), { x: 0, y: 0, width: res.w, height: res.h });
// doc.output('save'); // Works to open in a new
// window; todo: configure this and other export
// options to optionally work in this manner as
// opposed to opening a new tab
outputType = outputType || 'dataurlstring';
const obj = {issues, issueCodes, exportWindowName, outputType};
const obj = { issues, issueCodes, exportWindowName, outputType };
obj.output = doc.output(outputType, outputType === 'save' ? (exportWindowName || 'svg.pdf') : undefined);
svgContext_.call('exportedPDF', obj);
return obj;
@@ -861,14 +896,14 @@ export const uniquifyElemsMethod = function (g) {
// and we haven't tracked this ID yet
if (!(n.id in ids)) {
// add this id to our map
ids[n.id] = {elem: null, attrs: [], hrefs: []};
ids[n.id] = { elem: null, attrs: [], hrefs: [] };
}
ids[n.id].elem = n;
}
// now search for all attributes on this element that might refer
// to other elements
$.each(svgContext_.getrefAttrs(), function (i, attr) {
svgContext_.getrefAttrs().forEach(function(attr){
const attrnode = n.getAttributeNode(attr);
if (attrnode) {
// the incoming file has been sanitized, so we should be able to safely just strip off the leading #
@@ -877,7 +912,7 @@ export const uniquifyElemsMethod = function (g) {
if (refid) {
if (!(refid in ids)) {
// add this id to our map
ids[refid] = {elem: null, attrs: [], hrefs: []};
ids[refid] = { elem: null, attrs: [], hrefs: [] };
}
ids[refid].attrs.push(attrnode);
}
@@ -892,7 +927,7 @@ export const uniquifyElemsMethod = function (g) {
if (refid) {
if (!(refid in ids)) {
// add this id to our map
ids[refid] = {elem: null, attrs: [], hrefs: []};
ids[refid] = { elem: null, attrs: [], hrefs: [] };
}
ids[refid].hrefs.push(n);
}
@@ -903,7 +938,7 @@ export const uniquifyElemsMethod = function (g) {
// in ids, we now have a map of ids, elements and attributes, let's re-identify
for (const oldid in ids) {
if (!oldid) { continue; }
const {elem} = ids[oldid];
const { elem } = ids[oldid];
if (elem) {
const newid = svgContext_.getCanvas().getNextId();
@@ -911,7 +946,7 @@ export const uniquifyElemsMethod = function (g) {
elem.id = newid;
// remap all url() attributes
const {attrs} = ids[oldid];
const { attrs } = ids[oldid];
let j = attrs.length;
while (j--) {
const attr = attrs[j];
@@ -936,19 +971,22 @@ export const uniquifyElemsMethod = function (g) {
* @returns {void}
*/
export const setUseDataMethod = function (parent) {
let elems = $(parent);
let elems = parent;
if (parent.tagName !== 'use') {
elems = elems.find('use');
// elems = elems.find('use');
elems = elems.querySelectorAll('use');
}
elems.each(function () {
const id = svgContext_.getCanvas().getHref(this).substr(1);
Array.prototype.forEach.call(elems, function (el, _) {
const dataStorage = svgContext_.getDataStorage();
const id = svgContext_.getCanvas().getHref(el).substr(1);
const refElem = svgContext_.getCanvas().getElem(id);
if (!refElem) { return; }
$(this).data('ref', refElem);
dataStorage.put(el, 'ref', refElem);
if (refElem.tagName === 'symbol' || refElem.tagName === 'svg') {
$(this).data('symbol', refElem).data('ref', refElem);
dataStorage.put(el, 'symbol', refElem);
dataStorage.put(el, 'ref', refElem);
}
});
};
@@ -990,18 +1028,20 @@ export const removeUnusedDefElemsMethod = function () {
}
}
const defelems = $(defs).find('linearGradient, radialGradient, filter, marker, svg, symbol');
i = defelems.length;
while (i--) {
const defelem = defelems[i];
const {id} = defelem;
if (!defelemUses.includes(id)) {
// Not found, so remove (but remember)
svgContext_.setRemovedElements(id, defelem);
defelem.remove();
numRemoved++;
Array.prototype.forEach.call(defs, function (def, i) {
const defelems = def.querySelectorAll('linearGradient, radialGradient, filter, marker, svg, symbol');
i = defelems.length;
while (i--) {
const defelem = defelems[i];
const { id } = defelem;
if (!defelemUses.includes(id)) {
// Not found, so remove (but remember)
svgContext_.setRemovedElements(id, defelem);
defelem.remove();
numRemoved++;
}
}
}
});
return numRemoved;
};
@@ -1012,20 +1052,19 @@ export const removeUnusedDefElemsMethod = function () {
* @returns {void}
*/
export const convertGradientsMethod = function (elem) {
let elems = $(elem).find('linearGradient, radialGradient');
let elems = elem.querySelectorAll('linearGradient, radialGradient');
if (!elems.length && isWebkit()) {
// Bug in webkit prevents regular *Gradient selector search
elems = $(elem).find('*').filter(function () {
return (this.tagName.includes('Gradient'));
elems = Array.prototype.filter.call(elem.querySelectorAll('*'), function (curThis) {
return (curThis.tagName.includes('Gradient'));
});
}
elems.each(function () {
const grad = this;
if ($(grad).attr('gradientUnits') === 'userSpaceOnUse') {
Array.prototype.forEach.call(elems, function (grad) {
if (grad.getAttribute('gradientUnits') === 'userSpaceOnUse') {
const svgcontent = svgContext_.getSVGContent();
// TODO: Support more than one element with this ref by duplicating parent grad
const fillStrokeElems = $(svgcontent).find('[fill="url(#' + grad.id + ')"],[stroke="url(#' + grad.id + ')"]');
const fillStrokeElems = svgcontent.querySelectorAll('[fill="url(#' + grad.id + ')"],[stroke="url(#' + grad.id + ')"]');
if (!fillStrokeElems.length) { return; }
// get object's bounding box
@@ -1036,7 +1075,12 @@ export const convertGradientsMethod = function (elem) {
if (!bb) { return; }
if (grad.tagName === 'linearGradient') {
const gCoords = $(grad).attr(['x1', 'y1', 'x2', 'y2']);
const gCoords = {
x1: grad.getAttribute('x1'),
y1: grad.getAttribute('y1'),
x2: grad.getAttribute('x2'),
y2: grad.getAttribute('y2'),
};
// If has transform, convert
const tlist = grad.gradientTransform.baseVal;
@@ -1051,35 +1095,12 @@ export const convertGradientsMethod = function (elem) {
gCoords.y2 = pt2.y;
grad.removeAttribute('gradientTransform');
}
$(grad).attr({
x1: (gCoords.x1 - bb.x) / bb.width,
y1: (gCoords.y1 - bb.y) / bb.height,
x2: (gCoords.x2 - bb.x) / bb.width,
y2: (gCoords.y2 - bb.y) / bb.height
});
grad.setAttribute('x1', (gCoords.x1 - bb.x) / bb.width);
grad.setAttribute('y1', (gCoords.y1 - bb.y) / bb.height);
grad.setAttribute('x2', (gCoords.x2 - bb.x) / bb.width);
grad.setAttribute('y2', (gCoords.y2 - bb.y) / bb.height);
grad.removeAttribute('gradientUnits');
}
// else {
// Note: radialGradient elements cannot be easily converted
// because userSpaceOnUse will keep circular gradients, while
// objectBoundingBox will x/y scale the gradient according to
// its bbox.
//
// For now we'll do nothing, though we should probably have
// the gradient be updated as the element is moved, as
// inkscape/illustrator do.
//
// const gCoords = $(grad).attr(['cx', 'cy', 'r']);
//
// $(grad).attr({
// cx: (gCoords.cx - bb.x) / bb.width,
// cy: (gCoords.cy - bb.y) / bb.height,
// r: gCoords.r
// });
//
// grad.removeAttribute('gradientUnits');
// }
}
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import {NS} from '../common/namespaces.js';
import {text2xml} from '../common/utilities.js';
import {text2xml} from './utilities.js';
/**
* @function module:svgcanvas.svgRootElement svgRootElement the svg node and its children.

View File

@@ -0,0 +1,393 @@
/**
* Partial polyfill of `SVGTransformList`
* @module SVGTransformList
*
* @license MIT
*
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import {NS} from '../common/namespaces.js';
import {supportsNativeTransformLists} from '../common/browser.js';
const svgroot = document.createElementNS(NS.SVG, 'svg');
/**
* Helper function to convert `SVGTransform` to a string.
* @param {SVGTransform} xform
* @returns {string}
*/
function transformToString (xform) {
const m = xform.matrix;
let text = '';
switch (xform.type) {
case 1: // MATRIX
text = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')';
break;
case 2: // TRANSLATE
text = 'translate(' + m.e + ',' + m.f + ')';
break;
case 3: // SCALE
text = (m.a === m.d) ? `scale(${m.a})` : `scale(${m.a},${m.d})`;
break;
case 4: { // ROTATE
let cx = 0;
let cy = 0;
// this prevents divide by zero
if (xform.angle !== 0) {
const K = 1 - m.a;
cy = (K * m.f + m.b * m.e) / (K * K + m.b * m.b);
cx = (m.e - m.b * cy) / K;
}
text = 'rotate(' + xform.angle + ' ' + cx + ',' + cy + ')';
break;
}
}
return text;
}
/**
* Map of SVGTransformList objects.
*/
let listMap_ = {};
/**
* @interface module:SVGTransformList.SVGEditTransformList
* @property {Integer} numberOfItems unsigned long
*/
/**
* @function module:SVGTransformList.SVGEditTransformList#clear
* @returns {void}
*/
/**
* @function module:SVGTransformList.SVGEditTransformList#initialize
* @param {SVGTransform} newItem
* @returns {SVGTransform}
*/
/**
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
* @function module:SVGTransformList.SVGEditTransformList#getItem
* @param {Integer} index unsigned long
* @returns {SVGTransform}
*/
/**
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
* @function module:SVGTransformList.SVGEditTransformList#insertItemBefore
* @param {SVGTransform} newItem
* @param {Integer} index unsigned long
* @returns {SVGTransform}
*/
/**
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
* @function module:SVGTransformList.SVGEditTransformList#replaceItem
* @param {SVGTransform} newItem
* @param {Integer} index unsigned long
* @returns {SVGTransform}
*/
/**
* DOES NOT THROW DOMException, INDEX_SIZE_ERR.
* @function module:SVGTransformList.SVGEditTransformList#removeItem
* @param {Integer} index unsigned long
* @returns {SVGTransform}
*/
/**
* @function module:SVGTransformList.SVGEditTransformList#appendItem
* @param {SVGTransform} newItem
* @returns {SVGTransform}
*/
/**
* NOT IMPLEMENTED.
* @ignore
* @function module:SVGTransformList.SVGEditTransformList#createSVGTransformFromMatrix
* @param {SVGMatrix} matrix
* @returns {SVGTransform}
*/
/**
* NOT IMPLEMENTED.
* @ignore
* @function module:SVGTransformList.SVGEditTransformList#consolidate
* @returns {SVGTransform}
*/
/**
* SVGTransformList implementation for Webkit.
* These methods do not currently raise any exceptions.
* These methods also do not check that transforms are being inserted. This is basically
* implementing as much of SVGTransformList that we need to get the job done.
* @implements {module:SVGTransformList.SVGEditTransformList}
*/
export class SVGTransformList {
/**
* @param {Element} elem
* @returns {SVGTransformList}
*/
constructor (elem) {
this._elem = elem || null;
this._xforms = [];
// TODO: how do we capture the undo-ability in the changed transform list?
this._update = function () {
let tstr = '';
// /* const concatMatrix = */ svgroot.createSVGMatrix();
for (let i = 0; i < this.numberOfItems; ++i) {
const xform = this._list.getItem(i);
tstr += transformToString(xform) + ' ';
}
this._elem.setAttribute('transform', tstr);
};
this._list = this;
this._init = function () {
// Transform attribute parser
let str = this._elem.getAttribute('transform');
if (!str) { return; }
// TODO: Add skew support in future
const re = /\s*((scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/;
// const re = /\s*(?<xform>(?:scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/;
let m = true;
while (m) {
m = str.match(re);
str = str.replace(re, '');
if (m && m[1]) {
const x = m[1];
const bits = x.split(/\s*\(/);
const name = bits[0];
const valBits = bits[1].match(/\s*(.*?)\s*\)/);
valBits[1] = valBits[1].replace(/(\d)-/g, '$1 -');
const valArr = valBits[1].split(/[, ]+/);
const letters = 'abcdef'.split('');
/*
if (m && m.groups.xform) {
const x = m.groups.xform;
const [name, bits] = x.split(/\s*\(/);
const valBits = bits.match(/\s*(?<nonWhitespace>.*?)\s*\)/);
valBits.groups.nonWhitespace = valBits.groups.nonWhitespace.replace(
/(?<digit>\d)-/g, '$<digit> -'
);
const valArr = valBits.groups.nonWhitespace.split(/[, ]+/);
const letters = [...'abcdef'];
*/
const mtx = svgroot.createSVGMatrix();
Object.values(valArr).forEach(function (item, i) {
valArr[i] = Number.parseFloat(item);
if (name === 'matrix') {
mtx[letters[i]] = valArr[i];
}
});
const xform = svgroot.createSVGTransform();
const fname = 'set' + name.charAt(0).toUpperCase() + name.slice(1);
const values = name === 'matrix' ? [mtx] : valArr;
if (name === 'scale' && values.length === 1) {
values.push(values[0]);
} else if (name === 'translate' && values.length === 1) {
values.push(0);
} else if (name === 'rotate' && values.length === 1) {
values.push(0, 0);
}
xform[fname](...values);
this._list.appendItem(xform);
}
}
};
this._removeFromOtherLists = function (item) {
if (item) {
// Check if this transform is already in a transformlist, and
// remove it if so.
Object.values(listMap_).some((tl) => {
for (let i = 0, len = tl._xforms.length; i < len; ++i) {
if (tl._xforms[i] === item) {
tl.removeItem(i);
return true;
}
}
return false;
});
}
};
this.numberOfItems = 0;
}
/**
* @returns {void}
*/
clear () {
this.numberOfItems = 0;
this._xforms = [];
}
/**
* @param {SVGTransform} newItem
* @returns {void}
*/
initialize (newItem) {
this.numberOfItems = 1;
this._removeFromOtherLists(newItem);
this._xforms = [newItem];
}
/**
* @param {Integer} index unsigned long
* @throws {Error}
* @returns {SVGTransform}
*/
getItem (index) {
if (index < this.numberOfItems && index >= 0) {
return this._xforms[index];
}
const err = new Error('DOMException with code=INDEX_SIZE_ERR');
err.code = 1;
throw err;
}
/**
* @param {SVGTransform} newItem
* @param {Integer} index unsigned long
* @returns {SVGTransform}
*/
insertItemBefore (newItem, index) {
let retValue = null;
if (index >= 0) {
if (index < this.numberOfItems) {
this._removeFromOtherLists(newItem);
const newxforms = new Array(this.numberOfItems + 1);
// TODO: use array copying and slicing
let i;
for (i = 0; i < index; ++i) {
newxforms[i] = this._xforms[i];
}
newxforms[i] = newItem;
for (let j = i + 1; i < this.numberOfItems; ++j, ++i) {
newxforms[j] = this._xforms[i];
}
this.numberOfItems++;
this._xforms = newxforms;
retValue = newItem;
this._list._update();
} else {
retValue = this._list.appendItem(newItem);
}
}
return retValue;
}
/**
* @param {SVGTransform} newItem
* @param {Integer} index unsigned long
* @returns {SVGTransform}
*/
replaceItem (newItem, index) {
let retValue = null;
if (index < this.numberOfItems && index >= 0) {
this._removeFromOtherLists(newItem);
this._xforms[index] = newItem;
retValue = newItem;
this._list._update();
}
return retValue;
}
/**
* @param {Integer} index unsigned long
* @throws {Error}
* @returns {SVGTransform}
*/
removeItem (index) {
if (index < this.numberOfItems && index >= 0) {
const retValue = this._xforms[index];
const newxforms = new Array(this.numberOfItems - 1);
let i;
for (i = 0; i < index; ++i) {
newxforms[i] = this._xforms[i];
}
for (let j = i; j < this.numberOfItems - 1; ++j, ++i) {
newxforms[j] = this._xforms[i + 1];
}
this.numberOfItems--;
this._xforms = newxforms;
this._list._update();
return retValue;
}
const err = new Error('DOMException with code=INDEX_SIZE_ERR');
err.code = 1;
throw err;
}
/**
* @param {SVGTransform} newItem
* @returns {SVGTransform}
*/
appendItem (newItem) {
this._removeFromOtherLists(newItem);
this._xforms.push(newItem);
this.numberOfItems++;
this._list._update();
return newItem;
}
}
/**
* @function module:SVGTransformList.resetListMap
* @returns {void}
*/
export const resetListMap = function () {
listMap_ = {};
};
/**
* Removes transforms of the given element from the map.
* @function module:SVGTransformList.removeElementFromListMap
* @param {Element} elem - a DOM Element
* @returns {void}
*/
export let removeElementFromListMap = function (elem) {
if (elem.id && listMap_[elem.id]) {
delete listMap_[elem.id];
}
};
/**
* Returns an object that behaves like a `SVGTransformList` for the given DOM element.
* @function module:SVGTransformList.getTransformList
* @param {Element} elem - DOM element to get a transformlist from
* @todo The polyfill should have `SVGAnimatedTransformList` and this should use it
* @returns {SVGAnimatedTransformList|SVGTransformList}
*/
export const getTransformList = function (elem) {
if (!supportsNativeTransformLists()) {
const id = elem.id || 'temp';
let t = listMap_[id];
if (!t || id === 'temp') {
listMap_[id] = new SVGTransformList(elem);
listMap_[id]._init();
t = listMap_[id];
}
return t;
}
if (elem.transform) {
return elem.transform.baseVal;
}
if (elem.gradientTransform) {
return elem.gradientTransform.baseVal;
}
if (elem.patternTransform) {
return elem.patternTransform.baseVal;
}
return null;
};
/**
* @callback module:SVGTransformList.removeElementFromListMap
* @param {Element} elem
* @returns {void}
*/
/**
* Replace `removeElementFromListMap` for unit-testing.
* @function module:SVGTransformList.changeRemoveElementFromListMap
* @param {module:SVGTransformList.removeElementFromListMap} cb Passed a single argument `elem`
* @returns {void}
*/
export const changeRemoveElementFromListMap = function (cb) {
removeElementFromListMap = cb;
};

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* @module text-actions Tools for Text edit functions
* @license MIT
@@ -6,20 +5,18 @@
* @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';
} from './math.js';
import {
assignAttributes, getElem, getBBox as utilsGetBBox
} from '../common/utilities.js';
} from './utilities.js';
import {
supportsGoodTextCharPos
} from '../common/browser.js';
const $ = jQueryPluginSVG(jQuery);
let textActionsContext_ = null;
/**
@@ -31,7 +28,6 @@ export const init = function (textActionsContext) {
textActionsContext_ = textActionsContext;
};
/* eslint-disable jsdoc/require-property */
/**
* Group: Text edit functions
* Functions relating to editing text elements.
@@ -39,7 +35,6 @@ export const init = function (textActionsContext) {
* @memberof module:svgcanvas.SvgCanvas#
*/
export const textActionsMethod = (function () {
/* eslint-enable jsdoc/require-property */
let curtext;
let textinput;
let cursor;
@@ -58,7 +53,7 @@ export const textActionsMethod = (function () {
*/
function setCursor (index) {
const empty = (textinput.value === '');
$(textinput).focus();
textinput.focus();
if (!arguments.length) {
if (empty) {
@@ -81,7 +76,7 @@ export const textActionsMethod = (function () {
stroke: '#333',
'stroke-width': 1
});
cursor = getElem('selectorParentGroup').appendChild(cursor);
getElem('selectorParentGroup').append(cursor);
}
if (!blinker) {
@@ -263,15 +258,6 @@ export const textActionsMethod = (function () {
return out;
}
/*
// Not currently in use
function hideCursor () {
if (cursor) {
cursor.setAttribute('visibility', 'hidden');
}
}
*/
/**
*
* @param {Event} evt
@@ -279,7 +265,7 @@ cursor.setAttribute('visibility', 'hidden');
*/
function selectAll (evt) {
setSelection(0, curtext.textContent.length);
$(this).unbind(evt);
evt.target.removeEventListener('click', selectAll);
}
/**
@@ -303,9 +289,10 @@ cursor.setAttribute('visibility', 'hidden');
setSelection(first, last);
// Set tripleclick
$(evt.target).click(selectAll);
evt.target.addEventListener('click', selectAll);
setTimeout(function () {
$(evt.target).unbind('click', selectAll);
evt.target.removeEventListener('click', selectAll);
}, 300);
}
@@ -402,7 +389,7 @@ cursor.setAttribute('visibility', 'hidden');
textActionsContext_.getCanvas().textActions.init();
$(curtext).css('cursor', 'text');
curtext.style.cursor = 'text';
// if (supportsEditableText()) {
// curtext.setAttribute('editable', 'simple');
@@ -429,13 +416,13 @@ cursor.setAttribute('visibility', 'hidden');
textActionsContext_.setCurrentMode('select');
clearInterval(blinker);
blinker = null;
if (selblock) { $(selblock).attr('display', 'none'); }
if (cursor) { $(cursor).attr('visibility', 'hidden'); }
$(curtext).css('cursor', 'move');
if (selblock) { selblock.setAttribute('display', 'none'); }
if (cursor) { cursor.setAttribute('visibility', 'hidden'); }
curtext.style.cursor = 'move';
if (selectElem) {
textActionsContext_.getCanvas().clearSelection();
$(curtext).css('cursor', 'move');
curtext.style.cursor = 'move';
textActionsContext_.call('selected', [curtext]);
textActionsContext_.getCanvas().addToSelection([curtext], true);
@@ -445,7 +432,7 @@ cursor.setAttribute('visibility', 'hidden');
textActionsContext_.getCanvas().deleteSelectedElements();
}
$(textinput).blur();
textinput.blur();
curtext = false;
@@ -459,7 +446,6 @@ cursor.setAttribute('visibility', 'hidden');
*/
setInputElem (elem) {
textinput = elem;
// $(textinput).blur(hideCursor);
},
/**
* @returns {void}
@@ -470,10 +456,10 @@ cursor.setAttribute('visibility', 'hidden');
}
},
/**
* @param {Element} inputElem Not in use
* @param {Element} _inputElem Not in use
* @returns {void}
*/
init (inputElem) {
init (_inputElem) {
if (!curtext) { return; }
let i, end;
// if (supportsEditableText()) {
@@ -501,7 +487,8 @@ cursor.setAttribute('visibility', 'hidden');
chardata.length = len;
textinput.focus();
$(curtext).unbind('dblclick', selectWord).dblclick(selectWord);
curtext.removeEventListener("dblclick", selectWord);
curtext.addEventListener("dblclick", selectWord);
if (!len) {
end = {x: textbb.x + (textbb.width / 2), width: 0};

View File

@@ -8,16 +8,16 @@ import * as draw from './draw.js';
import * as hstry from './history.js';
import {
getRotationAngle, getBBox as utilsGetBBox, isNullish, setHref, getStrokedBBoxDefaultVisible
} from '../common/utilities.js';
} from './utilities.js';
import {
isGecko
} from '../common/browser.js';
import {
transformPoint, transformListToTransform
} from '../common/math.js';
} from './math.js';
import {
getTransformList
} from '../common/svgtransformlist.js';
} from './svgtransformlist.js';
const {
UndoManager, HistoryEventTypes
@@ -188,7 +188,13 @@ export const changeSelectedAttributeNoUndoMethod = function (attr, newValue, ele
// }
} else if (attr === '#href') {
setHref(elem, newValue);
} else { elem.setAttribute(attr, newValue); }
} else if (newValue) {
elem.setAttribute(attr, newValue);
} else if (typeof newValue === 'number') {
elem.setAttribute(attr, newValue);
} else {
elem.removeAttribute(attr);
}
// Go into "select" mode for text changes
// NOTE: Important that this happens AFTER elem.setAttribute() or else attributes like
@@ -204,12 +210,11 @@ export const changeSelectedAttributeNoUndoMethod = function (attr, newValue, ele
// 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);
}
if (isGecko() &&
elem.nodeName === 'text' &&
(/rotate/).test(elem.getAttribute('transform')) &&
(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

1368
src/svgcanvas/utilities.js Normal file

File diff suppressed because it is too large Load Diff