Merge branch 'SVG-Edit-master'

master
JFH 2020-12-05 19:20:10 +01:00
commit 116a86138f
103 changed files with 10249 additions and 7879 deletions

View File

@ -30,3 +30,7 @@ src/editor/jspdf/underscore-min.js
src/editor/extensions/ext-mathjax/mathjax
# jquery files
src/editor/svgicons/jQuery.svgIcons.js
src/editor/jgraduate/jQuery.jPicker.js

View File

@ -46,13 +46,6 @@ to join the project.
### [Try SVG-edit here](https://svg-edit.github.io/svgedit/dist/editor/index.html)
<!-- See the [latest release](https://svg-edit.github.io/svgedit/releases/latest/editor/svg-editor.html)
(or its [ES6-Module](https://svg-edit.github.io/svgedit/releases/latest/editor/svg-editor-es.html)
version, which requires a modern browser).
-->
See a working editor on [`master`](https://svg-edit.github.io/svgedit/src/editor/index.html)
version, which requires a modern browser).
We also build a systemJS version at [`master`](https://svg-edit.github.io/svgedit/dist/editor/system/index.html)
You may also obtain URLs for specific [releases](https://github.com/SVG-Edit/svgedit/releases).

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="421" height="20"><defs><style>text{font-size:11px;font-family:Verdana,DejaVu Sans,Geneva,sans-serif}text.shadow{fill:#010101;fill-opacity:.3}text.high{fill:#fff}</style><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#aaa" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="100%" height="100%" rx="3" fill="#fff"/></mask></defs><g id="bg" mask="url(#round)"><path fill="green" stroke="#000" d="M0 0h113v20H0zM113 0h109v20H113zM222 0h87v20h-87zM309 0h112v20H309z"/><path fill="url(#smooth)" d="M0 0h421v20H0z"/></g><g id="fg"><text class="shadow" x="5.5" y="15">Statements 51.1%</text><text class="high" x="5" y="14">Statements 51.1%</text><text class="shadow" x="118.5" y="15">Branches 40.83%</text><text class="high" x="118" y="14">Branches 40.83%</text><text class="shadow" x="227.5" y="15">Lines 51.81%</text><text class="high" x="227" y="14">Lines 51.81%</text><text class="shadow" x="314.5" y="15">Functions 58.62%</text><text class="high" x="314" y="14">Functions 58.62%</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="421" height="20"><defs><style>text{font-size:11px;font-family:Verdana,DejaVu Sans,Geneva,sans-serif}text.shadow{fill:#010101;fill-opacity:.3}text.high{fill:#fff}</style><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#aaa" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="100%" height="100%" rx="3" fill="#fff"/></mask></defs><g id="bg" mask="url(#round)"><path fill="green" stroke="#000" d="M0 0h120v20H0zM120 0h102v20H120zM222 0h87v20h-87zM309 0h112v20H309z"/><path fill="url(#smooth)" d="M0 0h421v20H0z"/></g><g id="fg"><text class="shadow" x="5.5" y="15">Statements 52.79%</text><text class="high" x="5" y="14">Statements 52.79%</text><text class="shadow" x="125.5" y="15">Branches 41.6%</text><text class="high" x="125" y="14">Branches 41.6%</text><text class="shadow" x="227.5" y="15">Lines 53.61%</text><text class="high" x="227" y="14">Lines 53.61%</text><text class="shadow" x="314.5" y="15">Functions 60.18%</text><text class="high" x="314" y="14">Functions 60.18%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><defs><style>text{font-size:11px;font-family:Verdana,DejaVu Sans,Geneva,sans-serif}text.shadow{fill:#010101;fill-opacity:.3}text.high{fill:#fff}</style><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#aaa" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="100%" height="100%" rx="3" fill="#fff"/></mask></defs><g id="bg" mask="url(#round)"><path fill="#696969" d="M0 0h41v20H0z"/><path fill="#4c1" d="M41 0h56v20H41z"/><path fill="url(#smooth)" d="M0 0h97v20H0z"/></g><g id="fg"><text class="shadow" x="5.5" y="15">Tests</text><text class="high" x="5" y="14">Tests</text><text class="shadow" x="46.5" y="15">139/139</text><text class="high" x="46" y="14">139/139</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><defs><style>text{font-size:11px;font-family:Verdana,DejaVu Sans,Geneva,sans-serif}text.shadow{fill:#010101;fill-opacity:.3}text.high{fill:#fff}</style><linearGradient id="smooth" x2="0" y2="100%"><stop offset="0" stop-color="#aaa" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="round"><rect width="100%" height="100%" rx="3" fill="#fff"/></mask></defs><g id="bg" mask="url(#round)"><path fill="#696969" d="M0 0h41v20H0z"/><path fill="#4c1" d="M41 0h56v20H41z"/><path fill="url(#smooth)" d="M0 0h97v20H0z"/></g><g id="fg"><text class="shadow" x="5.5" y="15">Tests</text><text class="high" x="5" y="14">Tests</text><text class="shadow" x="46.5" y="15">141/141</text><text class="high" x="46" y="14">141/141</text></g></svg>

Before

Width:  |  Height:  |  Size: 820 B

After

Width:  |  Height:  |  Size: 820 B

View File

@ -221,6 +221,7 @@ exports[`use various parts of svg-edit > check tool_italic #0`] = `
fill-opacity="1"
stroke-opacity="1"
id="svg_2"
transform="matrix(1 0 0 1 0 0)"
>
B
</text>
@ -285,6 +286,7 @@ exports[`use various parts of svg-edit > check tool_bold #0`] = `
fill-opacity="1"
stroke-opacity="1"
id="svg_2"
transform="matrix(1 0 0 1 0 0)"
>
B
</text>
@ -349,6 +351,7 @@ exports[`use various parts of svg-edit > check change color #0`] = `
fill-opacity="1"
stroke-opacity="1"
id="svg_2"
transform="matrix(1 0 0 1 0 0)"
>
B
</text>
@ -414,6 +417,7 @@ exports[`use various parts of svg-edit > check tool_start #0`] = `
fill-opacity="1"
stroke-opacity="1"
id="svg_2"
transform="matrix(1 0 0 1 0 0)"
>
B
</text>
@ -445,3 +449,203 @@ exports[`use various parts of svg-edit > check tool_start #0`] = `
</g>
</svg>
`;
exports[`use various parts of svg-edit > check tool_star #0`] = `
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
width="640"
height="480"
id="svgcontent"
overflow="visible"
x="640"
y="480"
viewBox="0 0 640 480"
>
<g class="layer" style="pointer-events:all">
<title style="pointer-events:inherit">Layer 1</title>
<rect
id="rect"
fill="#FF0000"
height="70"
stroke="#000000"
stroke-width="5"
width="94"
x="69.5"
y="51.5"
style="pointer-events:inherit"
fill-opacity="1"
stroke-opacity="1"
></rect>
<text
fill="#ffff00"
stroke="#000000"
stroke-width="0"
x="116"
y="87"
id="svg_1"
font-size="24"
font-family="serif"
text-anchor="middle"
xml:space="preserve"
fill-opacity="1"
stroke-opacity="1"
font-style="italic"
font-weight="bold"
>
B
</text>
<text
fill="#000000"
stroke="#000000"
stroke-width="0"
x="136"
y="107"
font-size="24"
font-family="serif"
text-anchor="middle"
xml:space="preserve"
fill-opacity="1"
stroke-opacity="1"
id="svg_2"
transform="matrix(1 0 0 1 0 0)"
>
B
</text>
<polygon
cx="407"
cy="45"
id="svg_3"
shape="star"
point="5"
r="13.333333333333334"
radialshift=""
r2="5.333333333333334"
orient="point"
fill="#000000"
strokecolor="#000000"
strokeWidth="0"
points="407,31.666666666666664 410.1348546788932,40.68524269666695 419.68075355060205,40.87977340833403 412.0723014202408,46.64809063666639 414.83713669723295,55.78689325833263 407,50.333333333333336 399.16286330276705,55.78689325833263 401.9276985797592,46.64809063666639 394.31924644939795,40.87977340833404 403.8651453211068,40.68524269666695 407,31.666666666666664 410.1348546788932,40.68524269666695 "
stroke="#000000"
stroke-width="0"
>
<animate
attributeName="opacity"
begin="indefinite"
dur="0.2"
fill="freeze"
to="1"
></animate>
</polygon>
</g>
</svg>
`;
exports[`use various parts of svg-edit > check tool_polygon #0`] = `
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
width="640"
height="480"
id="svgcontent"
overflow="visible"
x="640"
y="480"
viewBox="0 0 640 480"
>
<g class="layer" style="pointer-events:all">
<title style="pointer-events:inherit">Layer 1</title>
<rect
id="rect"
fill="#FF0000"
height="70"
stroke="#000000"
stroke-width="5"
width="94"
x="69.5"
y="51.5"
style="pointer-events:inherit"
fill-opacity="1"
stroke-opacity="1"
></rect>
<text
fill="#ffff00"
stroke="#000000"
stroke-width="0"
x="116"
y="87"
id="svg_1"
font-size="24"
font-family="serif"
text-anchor="middle"
xml:space="preserve"
fill-opacity="1"
stroke-opacity="1"
font-style="italic"
font-weight="bold"
>
B
</text>
<text
fill="#000000"
stroke="#000000"
stroke-width="0"
x="136"
y="107"
font-size="24"
font-family="serif"
text-anchor="middle"
xml:space="preserve"
fill-opacity="1"
stroke-opacity="1"
id="svg_2"
transform="matrix(1 0 0 1 0 0)"
>
B
</text>
<polygon
cx="407"
cy="45"
id="svg_3"
shape="star"
point="5"
r="13.333333333333334"
radialshift=""
r2="5.333333333333334"
orient="point"
fill="#000000"
strokecolor="#000000"
strokeWidth="0"
points="407,31.666666666666664 410.1348546788932,40.68524269666695 419.68075355060205,40.87977340833403 412.0723014202408,46.64809063666639 414.83713669723295,55.78689325833263 407,50.333333333333336 399.16286330276705,55.78689325833263 401.9276985797592,46.64809063666639 394.31924644939795,40.87977340833404 403.8651453211068,40.68524269666695 407,31.666666666666664 410.1348546788932,40.68524269666695 "
stroke="#000000"
stroke-width="0"
style="pointer-events:inherit"
fill-opacity="1"
stroke-opacity="1"
></polygon>
<polygon
cx="457"
cy="95"
id="svg_4"
shape="regularPoly"
sides="5"
orient="x"
edge="6.666666666666667"
fill="#000000"
strokecolor="#000000"
strokeWidth="5"
points="462.6710053890136,95 458.7524370403971,100.39344662916632 452.4120602650961,98.33333333333333 452.4120602650961,91.66666666666667 458.7524370403971,89.60655337083368 462.6710053890136,95 "
stroke="#000000"
stroke-width="5"
>
<animate
attributeName="opacity"
begin="indefinite"
dur="0.2"
fill="freeze"
to="1"
></animate>
</polygon>
</g>
</svg>
`;

View File

@ -11,13 +11,13 @@ describe('UI - Clipboard', function () {
cy.get('#tool_source').click();
cy.get('#svg_source_textarea')
.type('{selectall}')
.type('{selectall}', {force: true})
.type(`<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<circle cx="100" cy="100" r="50" fill="#FF0000" id="testCircle" stroke="#000000" stroke-width="5"/>
</g>
</svg>`, {parseSpecialCharSequences: false});
</svg>`, {force: true, parseSpecialCharSequences: false});
cy.get('#tool_source_save').click();
cy.get('#testCircle').should('exist');
cy.get('#svg_1').should('not.exist');
@ -25,20 +25,20 @@ describe('UI - Clipboard', function () {
// Copy.
cy.get('#testCircle').click().rightclick();
cy.get('#cmenu_canvas a[href="#copy"]').click();
cy.get('#cmenu_canvas a[href="#copy"]').click({force: true});
// Paste.
// Scrollbars fail to recenter in Cypress test. Works fine in reality.
// Thus forcing click is needed since workspace is mostly offscreen.
cy.get('#svgroot').rightclick({force: true});
cy.get('#cmenu_canvas a[href="#paste"]').click();
cy.get('#cmenu_canvas a[href="#paste"]').click({force: true});
cy.get('#testCircle').should('exist');
cy.get('#svg_1').should('exist');
cy.get('#svg_2').should('not.exist');
// Cut.
cy.get('#testCircle').click().rightclick();
cy.get('#cmenu_canvas a[href="#cut"]').click();
cy.get('#cmenu_canvas a[href="#cut"]').click({force: true});
cy.get('#testCircle').should('not.exist');
cy.get('#svg_1').should('exist');
cy.get('#svg_2').should('not.exist');
@ -47,16 +47,16 @@ describe('UI - Clipboard', function () {
// Scrollbars fail to recenter in Cypress test. Works fine in reality.
// Thus forcing click is needed since workspace is mostly offscreen.
cy.get('#svgroot').rightclick({force: true});
cy.get('#cmenu_canvas a[href="#paste"]').click();
cy.get('#cmenu_canvas a[href="#paste"]').click({force: true});
cy.get('#testCircle').should('not.exist');
cy.get('#svg_1').should('exist');
cy.get('#svg_2').should('exist');
// Delete.
cy.get('#svg_2').click().rightclick();
cy.get('#cmenu_canvas a[href="#delete"]').click();
cy.get('#cmenu_canvas a[href="#delete"]').click({force: true});
cy.get('#svg_1').click().rightclick();
cy.get('#cmenu_canvas a[href="#delete"]').click();
cy.get('#cmenu_canvas a[href="#delete"]').click({force: true});
cy.get('#svg_1').should('not.exist');
cy.get('#svg_2').should('not.exist');
});

View File

@ -12,11 +12,11 @@ describe('UI - Control Points', function () {
cy.get('#tool_source').click();
cy.get('#svg_source_textarea')
.type('{selectall}')
.type('{selectall}', {force: true})
.type(`<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<path d="m187,194a114,62 0 1 0 219,2" fill="#FF0000" stroke="#000000" stroke-width="5"/>
<path d="m187,194a114,62 0 1 0 219,2" id="svg_1" fill="#FF0000" stroke="#000000" stroke-width="5"/>
</g>
</svg>`, {force: true, parseSpecialCharSequences: false});
cy.get('#tool_source_save').click();

View File

@ -11,7 +11,7 @@ describe('Fix issue 408', function () {
it('should not throw when showing/saving svg content', function () {
cy.get('#tool_source').click();
cy.get('#svg_source_textarea')
.type('{selectall}')
.type('{selectall}', {force: true})
.type(`<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>

View File

@ -20,7 +20,7 @@ describe('use various parts of svg-edit', function () {
<title>Layer 1</title>
<rect id="rect" fill="#FF0000" height="70" stroke="#000000" stroke-width="5" width="94" x="69.5" y="51.5"/>
</g>
</svg>`, {parseSpecialCharSequences: false});
</svg>`, {force: true, parseSpecialCharSequences: false});
cy.get('#tool_source_save').click({force: true});
testSnapshot();
});
@ -70,7 +70,7 @@ describe('use various parts of svg-edit', function () {
.click({force: true});
testSnapshot();
});
it('check tool_start', function () {
it('check tool_star', function () {
cy.get('#tool_star')
.click({force: true});
cy.get('#svgcontent')
@ -79,4 +79,13 @@ describe('use various parts of svg-edit', function () {
.trigger('mouseup', {force: true});
cy.get('#svgcontent').toMatchSnapshot();
});
it('check tool_polygon', function () {
cy.get('#tool_polygon')
.click({force: true});
cy.get('#svgcontent')
.trigger('mousedown', {which: 1, pageX: 650, pageY: 200, force: true})
.trigger('mousemove', {which: 1, pageX: 650, pageY: 210, force: true})
.trigger('mouseup', {force: true});
cy.get('#svgcontent').toMatchSnapshot();
});
});

View File

@ -6,6 +6,7 @@ import '../../../instrumented/editor/jquery.min.js';
import {NS} from '../../../instrumented/common/namespaces.js';
import * as utilities from '../../../instrumented/common/utilities.js';
import * as pathModule from '../../../instrumented/svgcanvas/path.js';
import {Path, Segment} from '../../../instrumented/svgcanvas/path-method.js';
import {init as unitsInit} from '../../../instrumented/common/units.js';
describe('path', function () {
@ -50,7 +51,7 @@ describe('path', function () {
const [mockPathContext, mockUtilitiesContext] = getMockContexts();
pathModule.init(mockPathContext);
utilities.init(mockUtilitiesContext);
new pathModule.Path(path); // eslint-disable-line no-new
new Path(path); // eslint-disable-line no-new
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L');
assert.equal(path.pathSegList.getItem(1).x, 10);
@ -70,13 +71,13 @@ describe('path', function () {
const [mockPathContext, mockUtilitiesContext] = getMockContexts();
pathModule.init(mockPathContext);
utilities.init(mockUtilitiesContext);
new pathModule.Path(path); // eslint-disable-line no-new
new Path(path); // eslint-disable-line no-new
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L');
assert.equal(path.pathSegList.getItem(1).x, 10);
assert.equal(path.pathSegList.getItem(1).y, 11);
const segment = new pathModule.Segment(1, path.pathSegList.getItem(1));
const segment = new Segment(1, path.pathSegList.getItem(1));
segment.setType(SVGPathSeg.PATHSEG_LINETO_REL, [30, 31]);
assert.equal(segment.item.pathSegTypeAsLetter, 'l');
assert.equal(segment.item.x, 30);
@ -98,8 +99,8 @@ describe('path', function () {
const [mockPathContext, mockUtilitiesContext] = getMockContexts(svg);
pathModule.init(mockPathContext);
utilities.init(mockUtilitiesContext);
const segment = new pathModule.Segment(1, path.pathSegList.getItem(1));
segment.path = new pathModule.Path(path);
const segment = new Segment(1, path.pathSegList.getItem(1));
segment.path = new Path(path);
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C');
assert.equal(path.pathSegList.getItem(1).x1, 11);
@ -126,13 +127,13 @@ describe('path', function () {
const [mockPathContext, mockUtilitiesContext] = getMockContexts();
pathModule.init(mockPathContext);
utilities.init(mockUtilitiesContext);
new pathModule.Path(path); // eslint-disable-line no-new
new Path(path); // eslint-disable-line no-new
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L');
assert.equal(path.pathSegList.getItem(1).x, 10);
assert.equal(path.pathSegList.getItem(1).y, 11);
const segment = new pathModule.Segment(1, path.pathSegList.getItem(1));
const segment = new Segment(1, path.pathSegList.getItem(1));
segment.move(-3, 4);
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L');
assert.equal(path.pathSegList.getItem(1).x, 7);
@ -146,7 +147,7 @@ describe('path', function () {
const [mockPathContext, mockUtilitiesContext] = getMockContexts();
pathModule.init(mockPathContext);
utilities.init(mockUtilitiesContext);
new pathModule.Path(path); // eslint-disable-line no-new
new Path(path); // eslint-disable-line no-new
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C');
assert.equal(path.pathSegList.getItem(1).x1, 11);
@ -156,7 +157,7 @@ describe('path', function () {
assert.equal(path.pathSegList.getItem(1).x, 15);
assert.equal(path.pathSegList.getItem(1).y, 16);
const segment = new pathModule.Segment(1, path.pathSegList.getItem(1));
const segment = new Segment(1, path.pathSegList.getItem(1));
segment.moveCtrl(1, 100, -200);
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C');
assert.equal(path.pathSegList.getItem(1).x1, 111);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

72
dist/editor/index.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1099
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -134,99 +134,100 @@
"not ios_saf < 10"
],
"dependencies": {
"@babel/polyfill": "^7.12.1",
"canvg": "^3.0.6",
"core-js": "^3.6.5",
"jspdf": "^2.1.1",
"pathseg": "^1.2.0",
"regenerator-runtime": "^0.13.7",
"svg2pdf.js": "^2.0.0"
"@babel/polyfill": "7.12.1",
"canvg": "3.0.7",
"core-js": "3.7.0",
"jspdf": "2.1.1",
"pathseg": "1.2.0",
"regenerator-runtime": "0.13.7",
"svg2pdf.js": "2.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/preset-env": "^7.12.1",
"@babel/register": "^7.12.1",
"@babel/runtime-corejs3": "^7.12.1",
"@cypress/code-coverage": "^3.8.1",
"@cypress/fiddle": "^1.18.3",
"@fintechstudios/eslint-plugin-chai-as-promised": "^3.0.2",
"@hkdobrev/run-if-changed": "^0.3.1",
"@mysticatea/eslint-plugin": "^13.0.0",
"@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-dynamic-import-vars": "^1.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-replace": "^2.3.3",
"@rollup/plugin-url": "^5.0.1",
"babel-plugin-transform-object-rest-spread": "^7.0.0-beta.3",
"copyfiles": "^2.4.0",
"core-js-bundle": "^3.6.5",
"coveradge": "^0.6.0",
"cp-cli": "^2.0.0",
"cross-var": "^1.1.0",
"cypress": "^5.4.0",
"cypress-axe": "^0.8.1",
"cypress-multi-reporters": "^1.4.0",
"cypress-plugin-snapshots": "^1.4.4",
"@babel/core": "7.12.3",
"@babel/preset-env": "7.12.1",
"@babel/register": "7.12.1",
"@babel/runtime-corejs3": "7.12.5",
"@cypress/code-coverage": "3.8.3",
"@cypress/fiddle": "1.18.4",
"@fintechstudios/eslint-plugin-chai-as-promised": "3.0.2",
"@hkdobrev/run-if-changed": "0.3.1",
"@mysticatea/eslint-plugin": "13.0.0",
"@rollup/plugin-babel": "5.2.1",
"@rollup/plugin-commonjs": "15.1.0",
"@rollup/plugin-dynamic-import-vars": "1.1.0",
"@rollup/plugin-node-resolve": "9.0.0",
"@rollup/plugin-replace": "2.3.4",
"@rollup/plugin-url": "5.0.1",
"axe-core": "4.0.2",
"babel-plugin-transform-object-rest-spread": "7.0.0-beta.3",
"copyfiles": "2.4.0",
"core-js-bundle": "3.7.0",
"coveradge": "0.6.0",
"cp-cli": "2.0.0",
"cross-var": "1.1.0",
"cypress": "5.5.0",
"cypress-axe": "0.10.0",
"cypress-multi-reporters": "1.4.0",
"cypress-plugin-snapshots": "1.4.4",
"deparam": "git+https://github.com/brettz9/deparam.git#updates",
"es-dev-commonjs-transformer": "^0.2.0",
"es-dev-server": "^1.57.8",
"es-dev-commonjs-transformer": "0.2.0",
"es-dev-server": "1.57.8",
"es-dev-server-rollup": "0.0.8",
"eslint": "^7.11.0",
"eslint-config-ash-nazg": "^22.8.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-array-func": "^3.1.7",
"eslint-plugin-chai-expect": "^2.2.0",
"eslint-plugin-chai-expect-keywords": "^2.0.1",
"eslint-plugin-chai-friendly": "^0.6.0",
"eslint-plugin-compat": "^3.8.0",
"eslint-plugin-cypress": "^2.11.2",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-html": "^6.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsdoc": "^30.7.3",
"eslint-plugin-markdown": "^1.0.2",
"eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-mocha-cleanup": "^1.8.0",
"eslint-plugin-no-unsanitized": "^3.1.4",
"eslint-plugin-no-use-extend-native": "^0.5.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-sonarjs": "^0.5.0",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-unicorn": "^22.0.0",
"husky": "^4.3.0",
"imageoptim-cli": "^3.0.2",
"jamilih": "^0.53.2",
"jsdoc": "^3.6.6",
"license-badger": "^0.18.0",
"lint-staged": "^10.4.1",
"mocha": "^8.2.0",
"mocha-badge-generator": "^0.8.0",
"mochawesome": "^6.1.1",
"mochawesome-merge": "^4.2.0",
"mochawesome-report-generator": "^5.1.0",
"node-static": "^0.7.11",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^6.0.1",
"promise-fs": "^2.1.1",
"eslint": "7.13.0",
"eslint-config-ash-nazg": "22.10.0",
"eslint-config-standard": "16.0.1",
"eslint-plugin-array-func": "3.1.7",
"eslint-plugin-chai-expect": "2.2.0",
"eslint-plugin-chai-expect-keywords": "2.0.1",
"eslint-plugin-chai-friendly": "0.6.0",
"eslint-plugin-compat": "3.8.0",
"eslint-plugin-cypress": "2.11.2",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-html": "6.1.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsdoc": "30.7.7",
"eslint-plugin-markdown": "1.0.2",
"eslint-plugin-mocha": "8.0.0",
"eslint-plugin-mocha-cleanup": "1.9.1",
"eslint-plugin-no-unsanitized": "3.1.4",
"eslint-plugin-no-use-extend-native": "0.5.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "4.2.1",
"eslint-plugin-sonarjs": "0.5.0",
"eslint-plugin-standard": "4.0.2",
"eslint-plugin-unicorn": "23.0.0",
"husky": "4.3.0",
"imageoptim-cli": "3.0.2",
"jamilih": "0.53.2",
"jsdoc": "3.6.6",
"license-badger": "0.18.0",
"lint-staged": "10.5.1",
"mocha": "8.2.1",
"mocha-badge-generator": "0.9.0",
"mochawesome": "6.2.1",
"mochawesome-merge": "4.2.0",
"mochawesome-report-generator": "5.1.0",
"node-static": "0.7.11",
"npm-run-all": "4.1.5",
"nyc": "15.1.0",
"open-cli": "6.0.1",
"promise-fs": "2.1.1",
"qr-manipulation": "git+https://github.com/brettz9/qr-manipulation.git",
"query-result": "git+https://github.com/WebReflection/query-result.git",
"remark-cli": "^9.0.0",
"remark-lint-ordered-list-marker-value": "^2.0.1",
"requirejs": "^2.3.6",
"rimraf": "^3.0.2",
"rollup": "2.32.0",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-filesize": "^9.0.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-progress": "^1.1.2",
"rollup-plugin-re": "^1.0.7",
"rollup-plugin-terser": "^7.0.2",
"stackblur-canvas": "^2.4.0",
"systemjs": "^6.7.1",
"typescript": "^4.0.3",
"underscore": "^1.11.0"
"remark-cli": "9.0.0",
"remark-lint-ordered-list-marker-value": "2.0.1",
"requirejs": "2.3.6",
"rimraf": "3.0.2",
"rollup": "2.33.1",
"rollup-plugin-copy": "3.3.0",
"rollup-plugin-filesize": "9.0.2",
"rollup-plugin-node-polyfills": "0.2.1",
"rollup-plugin-progress": "1.1.2",
"rollup-plugin-re": "1.0.7",
"rollup-plugin-terser": "7.0.2",
"stackblur-canvas": "2.4.0",
"systemjs": "6.7.1",
"typescript": "4.0.5",
"underscore": "1.11.0"
}
}

View File

@ -28,11 +28,7 @@ function transformToString (xform) {
text = 'translate(' + m.e + ',' + m.f + ')';
break;
case 3: // SCALE
if (m.a === m.d) {
text = 'scale(' + m.a + ')';
} else {
text = 'scale(' + m.a + ',' + m.d + ')';
}
text = m.a === m.d ? 'scale(' + m.a + ')' : 'scale(' + m.a + ',' + m.d + ')';
break;
case 4: { // ROTATE
let cx = 0;

View File

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

View File

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

View File

@ -189,10 +189,7 @@ export default {
let viewBox = '0 0 100 100';
let markerWidth = 5;
let markerHeight = 5;
let seType;
if (val.substr(0, 1) === '\\') {
seType = val.substr(1);
} else { seType = 'textmarker'; }
const seType = val.substr(0, 1) === '\\' ? val.substr(1) : 'textmarker';
if (!markerTypes[seType]) { return undefined; } // an unknown type!

View File

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

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

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

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

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

View File

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

1388
src/svgcanvas/event.js Normal file

File diff suppressed because it is too large Load Diff

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

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

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

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More