Merge branch 'master' into perf

master
JFH 2021-11-12 15:59:21 +01:00
commit 8eecf8b229
26 changed files with 403 additions and 690 deletions

View File

@ -63,7 +63,9 @@ module.exports = {
rules: { rules: {
// with ci, instrumented is not created before linter // with ci, instrumented is not created before linter
"import/no-unresolved": [ 2, { ignore: [ 'instrumented' ] } ], "import/no-unresolved": [ 2, { ignore: [ 'instrumented' ] } ],
"node/no-missing-import": 0 "node/no-missing-import": 0,
"node/no-unpublished-import": 0,
"node/no-unpublished-require": 0
} }
}, },
{ {

View File

@ -1,19 +1,6 @@
ignore
screencasts
.github/ISSUE_TEMPLATE/bug_report.md
gh-disabled-workflows
build
lgtm.yml lgtm.yml
cypress/**
cypress.env.json
coverage/** coverage/**
.nyc_output .nyc_output
instrumented/** instrumented/**
releases
tools
.eslintcache .eslintcache
node_modules/**

View File

@ -1,5 +0,0 @@
{
"plugins": {
"lint-ordered-list-marker-value": "one"
}
}

View File

@ -4,6 +4,7 @@ Jeff Schiller <codedread@gmail.com>
Vidar Hokstad <vidar.hokstad@gmail.com> Vidar Hokstad <vidar.hokstad@gmail.com>
Alexis Deveria <adeveria@gmail.com> Alexis Deveria <adeveria@gmail.com>
Brett Zamir <brettz9@yahoo.com> Brett Zamir <brettz9@yahoo.com>
Optimistik SAS <contact@optimistik.io>
Translation credits: Translation credits:

View File

@ -1,11 +1,11 @@
# SVG-Edit CHANGES # SVG-Edit CHANGES
## 7.0.0 (preview - work in progress) ## 7.0.0
- New UI - New UI
- Rearchitecture the code (more modular) - Rearchitecture the code (more modular)
- simplify and refresh the build process - Simplify and refresh the build process
- Introduce Web Component to replace jQuery UI - Introduce Web Component to replace jQuery UI
- update dependencies - Update dependencies
## 6.0.0 (unreleased) ## 6.0.0 (unreleased)
- Project: Add `FUNDING.yml` to accept contributions - Project: Add `FUNDING.yml` to accept contributions

View File

@ -1,6 +1,6 @@
<img src="https://svg-edit.github.io/svgedit/src/editor/images/logo.svg" width="50" height="50" /> <img src="https://svg-edit.github.io/svgedit/src/editor/images/logo.svg" width="50" height="50" />
# SVG-Edit # SVGEdit
[![npm](https://img.shields.io/npm/v/svgedit.svg)](https://www.npmjs.com/package/svgedit) [![npm](https://img.shields.io/npm/v/svgedit.svg)](https://www.npmjs.com/package/svgedit)
[![Dependencies](https://img.shields.io/david/SVG-Edit/svgedit.svg)](https://david-dm.org/SVG-Edit/svgedit) [![Dependencies](https://img.shields.io/david/SVG-Edit/svgedit.svg)](https://david-dm.org/SVG-Edit/svgedit)
@ -24,19 +24,19 @@ works in any modern browser.
![screenshot](docs/screenshot.png) ![screenshot](docs/screenshot.png)
[](https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg) [](https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg)
## Help wanted ## Contributions
SVG-Edit is the most popular open source SVG editor. It was started more than 10 years ago by a fantastic team of developers. Unfortunately, the product was not maintained for a quite long period. We decided to give this tool a new life by refreshing many aspects. SVGEdit is the most popular open source SVG editor. It was started more than 10 years ago by a fantastic team of developers. Unfortunately, the product was not maintained for a quite long period. We decided to give this tool a new life by refreshing many aspects.
If you can help us to maintain SVG-Edit, you are more than welcome! Please let us know with an issue or a discussions if you wish to contribute.
## Demo ## Demo
Thanks to Netlify, you can test the following builds: Thanks to **Netlify**, you can test the following builds:
### [Try SVG-edit V7-preview here](https://svgedit.netlify.app/editor/index.html) ### [Try SVGEdit 7.0.0 here](https://svgedit.netlify.app/editor/index.html)
[Try SVG-edit 5.1.0 here](https://6098683962bf91702907ee33--svgedit.netlify.app/editor/svg-editor.html) [Try SVGEdit 5.1.0 here](https://6098683962bf91702907ee33--svgedit.netlify.app/editor/svg-editor.html)
[Try SVG-edit 6.1.0 here](https://60a0000fc9900b0008fd268d--svgedit.netlify.app/editor/index.html) [Try SVGEdit 6.1.0 here](https://60a0000fc9900b0008fd268d--svgedit.netlify.app/editor/index.html)
## Installation ## Installation
@ -47,33 +47,20 @@ Thanks to Netlify, you can test the following builds:
1. run `npm run start` to start a local server 1. run `npm run start` to start a local server
1. Use your browser to access `http://localhost:8000/src/editor/index.html` 1. Use your browser to access `http://localhost:8000/src/editor/index.html`
### Integrating SVG-edit into your own application ### Integrating SVGEdit into your own application
V7 is changing significantly the way to integrate and customize SVG-Edit. The documentation will be detailed here. V7 is changing significantly the way to integrate and customize SVG-Edit. You can have a look to index.html to see how you can insert a div element into your HTML code and inject the editor into the div.
SVG-Edit is made of two major components: SVG-Edit is made of two major components:
1. The "svgcanvas" that takes care of the underlying svg edition. It can be used to build your own editor. See example in the demos folder or the svg-edit-react repository. 1. The "svgcanvas" that takes care of the underlying svg edition. It can be used to build your own editor. See example in the demos folder or the svg-edit-react repository.
1. The "editor" that takes care of the editor UI (menus, buttons, etc.) 1. The "editor" that takes care of the editor UI (menus, buttons, etc.)
For earlier versions of SVG-Edit, please look in their respective branches. For earlier versions of SVGEdit, please look in their respective branches.
## Supported browsers ## Supported browsers
Developments and Continuous Integration are done with a **Chrome** environment. Chrome, FireFox and Safari recent versions are supported (in the meaning that we will try to fix bugs for these browsers).
- Opera 59+, Support for old browsers may require to use an older version of the package. However, please open an issue if you need support for a specific version of your browser so the project team can decide if we should support with the latest version.
- Chrome 75+,
- FireFox 68+,
- Safari 11+
- Edge 18+
Support for old browsers may require to use an older version of the package. However,
please open an issue if you need support for a specific version of your browser so
the project team can decide if we should support with the latest version.
## Further reading and more information ## Further reading and more information
* Participate in [discussions](https://github.com/SVG-Edit/svgedit/discussions) * Participate in [discussions](https://github.com/SVG-Edit/svgedit/discussions)
* See [docs](docs/) for more documentation. See the
[JSDocs for our latest release](https://svg-edit.github.io/svgedit/releases/latest/docs/jsdoc/index.html).
* [Acknowledgements](docs/Acknowledgements.md) lists open source projects
used in svg-edit.
* See [AUTHORS](AUTHORS) file for authors. * See [AUTHORS](AUTHORS) file for authors.
* [StackOverflow](https://stackoverflow.com/tags/svg-edit) group. * [StackOverflow](https://stackoverflow.com/tags/svg-edit) group.

View File

@ -1,623 +0,0 @@
/**
* @file ext-markers.js
*
* @license Apache-2.0
*
* @copyright 2010 Will Schleter based on ext-arrows.js by Copyright(c) 2010 Alexis Deveria
*
* This extension provides for the addition of markers to the either end
* or the middle of a line, polyline, path, polygon.
*
* Markers may be either a graphic or arbitary text
*
* to simplify the coding and make the implementation as robust as possible,
* markers are not shared - every object has its own set of markers.
* this relationship is maintained by a naming convention between the
* ids of the markers and the ids of the object
*
* The following restrictions exist for simplicty of use and programming
* objects and their markers to have the same color
* marker size is fixed
* text marker font, size, and attributes are fixed
* an application specific attribute - se_type - is added to each marker element
* to store the type of marker
*
* @todo
* remove some of the restrictions above
* add option for keeping text aligned to horizontal
* add support for dimension extension lines
*
*/
const loadExtensionTranslation = async function (lang) {
let translationModule;
try {
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.error(`Missing translation (${lang}) - using 'en'`);
translationModule = await import(`./locale/en.js`);
}
return translationModule.default;
};
export default {
name: 'markers',
async init (S) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const { $ } = S;
const { svgCanvas } = svgEditor;
const { $id } = svgCanvas;
const // {svgcontent} = S,
addElem = svgCanvas.addSVGElementFromJson;
const mtypes = [ 'start', 'mid', 'end' ];
const markerPrefix = 'se_marker_';
const idPrefix = 'mkr_';
// note - to add additional marker types add them below with a unique id
// and add the associated icon(s) to marker-icons.svg
// the geometry is normalized to a 100x100 box with the origin at lower left
// Safari did not like negative values for low left of viewBox
// remember that the coordinate system has +y downward
const markerTypes = {
nomarker: {},
leftarrow:
{ element: 'path', attr: { d: 'M0,50 L100,90 L70,50 L100,10 Z' } },
rightarrow:
{ element: 'path', attr: { d: 'M100,50 L0,90 L30,50 L0,10 Z' } },
textmarker:
{ element: 'text', attr: {
x: 0, y: 0, 'stroke-width': 0, stroke: 'none',
'font-size': 75, 'font-family': 'serif', 'text-anchor': 'left',
'xml:space': 'preserve'
} },
forwardslash:
{ element: 'path', attr: { d: 'M30,100 L70,0' } },
reverseslash:
{ element: 'path', attr: { d: 'M30,0 L70,100' } },
verticalslash:
{ element: 'path', attr: { d: 'M50,0 L50,100' } },
box:
{ element: 'path', attr: { d: 'M20,20 L20,80 L80,80 L80,20 Z' } },
star:
{ element: 'path', attr: { d: 'M10,30 L90,30 L20,90 L50,10 L80,90 Z' } },
xmark:
{ element: 'path', attr: { d: 'M20,80 L80,20 M80,80 L20,20' } },
triangle:
{ element: 'path', attr: { d: 'M10,80 L50,20 L80,80 Z' } },
mcircle:
{ element: 'circle', attr: { r: 30, cx: 50, cy: 50 } }
};
// duplicate shapes to support unfilled (open) marker types with an _o suffix
[ 'leftarrow', 'rightarrow', 'box', 'star', 'mcircle', 'triangle' ].forEach((v) => {
markerTypes[v + '_o'] = markerTypes[v];
});
/**
* @param {Element} elem - A graphic element will have an attribute like marker-start
* @param {"marker-start"|"marker-mid"|"marker-end"} attr
* @returns {Element} The marker element that is linked to the graphic element
*/
function getLinked (elem, attr) {
const str = elem.getAttribute(attr);
if (!str) { return null; }
const m = str.match(/\(#(.*)\)/);
// const m = str.match(/\(#(?<id>.+)\)/);
// if (!m || !m.groups.id) {
if (!m || m.length !== 2) {
return null;
}
return svgCanvas.getElem(m[1]);
// return svgCanvas.getElem(m.groups.id);
}
/**
*
* @param {"start"|"mid"|"end"} pos
* @param {string} id
* @returns {void}
*/
function setIcon (pos, id) {
if (id.substr(0, 1) !== '\\') { id = '\\textmarker'; }
const ci = idPrefix + pos + '_' + id.substr(1);
svgEditor.setIcon('cur_' + pos + '_marker_list', $id(ci).children);
$id(ci).classList.add('current');
const siblings = Array.prototype.filter.call($id(ci).parentNode.children, function(child){
return child !== $id(ci);
});
Array.from(siblings).forEach(function(sibling) {
sibling.classList.remove('current');
});
}
let selElems;
/**
* Toggles context tool panel off/on. Sets the controls with the
* selected element's settings.
* @param {boolean} on
* @returns {void}
*/
function showPanel (on) {
$id('marker_panel').style.display = (on) ? 'block' : 'none';
if (on) {
const el = selElems[0];
let val; let ci;
$.each(mtypes, function (i, pos) {
const m = getLinked(el, 'marker-' + pos);
const txtbox = $id(pos + '_marker');
if (!m) {
val = '\\nomarker';
ci = val;
txtbox.style.display = 'none';
} else {
if (!m.attributes.se_type) { return; } // not created by this extension
val = '\\' + m.attributes.se_type.textContent;
ci = val;
if (val === '\\textmarker') {
val = m.lastChild.textContent;
// txtbox.show(); // show text box
} else {
txtbox.style.display = 'none';
}
}
txtbox.value = val;
setIcon(pos, ci);
});
}
}
/**
* @param {string} id
* @param {""|"\\nomarker"|"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} val
* @returns {SVGMarkerElement}
*/
function addMarker (id, val) {
const txtBoxBg = '#ffffff';
const txtBoxBorder = 'none';
const txtBoxStrokeWidth = 0;
let marker = svgCanvas.getElem(id);
if (marker) { return undefined; }
if (val === '' || val === '\\nomarker') { return undefined; }
const el = selElems[0];
const color = el.getAttribute('stroke');
// NOTE: Safari didn't like a negative value in viewBox
// so we use a standardized 0 0 100 100
// with 50 50 being mapped to the marker position
const strokeWidth = 10;
let refX = 50;
let refY = 50;
let viewBox = '0 0 100 100';
let markerWidth = 5;
let markerHeight = 5;
const seType = (val.substr(0, 1) === '\\') ? val.substr(1) : 'textmarker';
if (!markerTypes[seType]) { return undefined; } // an unknown type!
// create a generic marker
marker = addElem({
element: 'marker',
attr: {
id,
markerUnits: 'strokeWidth',
orient: 'auto',
style: 'pointer-events:none',
se_type: seType
}
});
if (seType !== 'textmarker') {
const mel = addElem(markerTypes[seType]);
const fillcolor = (seType.substr(-2) === '_o')
? 'none'
: color;
mel.setAttribute('fill', fillcolor);
mel.setAttribute('stroke', color);
mel.setAttribute('stroke-width', strokeWidth);
marker.append(mel);
} else {
const text = addElem(markerTypes[seType]);
// have to add text to get bounding box
text.textContent = val;
const tb = text.getBBox();
// alert(tb.x + ' ' + tb.y + ' ' + tb.width + ' ' + tb.height);
const pad = 1;
const bb = tb;
bb.x = 0;
bb.y = 0;
bb.width += pad * 2;
bb.height += pad * 2;
// shift text according to its size
text.setAttribute('x', pad);
text.setAttribute('y', bb.height - pad - tb.height / 4); // kludge?
text.setAttribute('fill', color);
refX = bb.width / 2 + pad;
refY = bb.height / 2 + pad;
viewBox = bb.x + ' ' + bb.y + ' ' + bb.width + ' ' + bb.height;
markerWidth = bb.width / 10;
markerHeight = bb.height / 10;
const box = addElem({
element: 'rect',
attr: {
x: bb.x,
y: bb.y,
width: bb.width,
height: bb.height,
fill: txtBoxBg,
stroke: txtBoxBorder,
'stroke-width': txtBoxStrokeWidth
}
});
marker.setAttribute('orient', 0);
marker.append(box, text);
}
marker.setAttribute('viewBox', viewBox);
marker.setAttribute('markerWidth', markerWidth);
marker.setAttribute('markerHeight', markerHeight);
marker.setAttribute('refX', refX);
marker.setAttribute('refY', refY);
svgCanvas.findDefs().append(marker);
return marker;
}
/**
* @param {Element} elem
* @returns {SVGPolylineElement}
*/
function convertline (elem) {
// this routine came from the connectors extension
// it is needed because midpoint markers don't work with line elements
if (elem.tagName !== 'line') { return elem; }
// Convert to polyline to accept mid-arrow
const x1 = Number(elem.getAttribute('x1'));
const x2 = Number(elem.getAttribute('x2'));
const y1 = Number(elem.getAttribute('y1'));
const y2 = Number(elem.getAttribute('y2'));
const { id } = elem;
const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' ');
const pline = addElem({
element: 'polyline',
attr: {
points: (x1 + ',' + y1 + midPt + x2 + ',' + y2),
stroke: elem.getAttribute('stroke'),
'stroke-width': elem.getAttribute('stroke-width'),
fill: 'none',
opacity: elem.getAttribute('opacity') || 1
}
});
$.each(mtypes, function (i, pos) { // get any existing marker definitions
const nam = 'marker-' + pos;
const m = elem.getAttribute(nam);
if (m) { pline.setAttribute(nam, elem.getAttribute(nam)); }
});
const batchCmd = new S.BatchCommand();
batchCmd.addSubCommand(new S.RemoveElementCommand(elem, elem.parentNode));
batchCmd.addSubCommand(new S.InsertElementCommand(pline));
elem.insertAdjacentElement('afterend', pline);
elem.remove();
svgCanvas.clearSelection();
pline.id = id;
svgCanvas.addToSelection([ pline ]);
S.addCommandToHistory(batchCmd);
return pline;
}
/**
*
* @returns {void}
*/
function setMarker () {
const poslist = { start_marker: 'start', mid_marker: 'mid', end_marker: 'end' };
const pos = poslist[this.id];
const markerName = 'marker-' + pos;
const el = selElems[0];
const marker = getLinked(el, markerName);
if (marker) { marker.remove(); }
el.removeAttribute(markerName);
let val = this.value;
if (val === '') { val = '\\nomarker'; }
if (val === '\\nomarker') {
setIcon(pos, val);
svgCanvas.call('changed', selElems);
return;
}
// Set marker on element
const id = markerPrefix + pos + '_' + el.id;
addMarker(id, val);
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
if (el.tagName === 'line' && pos === 'mid') {
convertline(el);
}
svgCanvas.call('changed', selElems);
setIcon(pos, val);
}
/**
* Called when the main system modifies an object. This routine changes
* the associated markers to be the same color.
* @param {Element} elem
* @returns {void}
*/
function colorChanged (elem) {
const color = elem.getAttribute('stroke');
$.each(mtypes, function (i, pos) {
const marker = getLinked(elem, 'marker-' + pos);
if (!marker) { return; }
if (!marker.attributes.se_type) { return; } // not created by this extension
const ch = marker.lastElementChild;
if (!ch) { return; }
const curfill = ch.getAttribute('fill');
const curstroke = ch.getAttribute('stroke');
if (curfill && curfill !== 'none') { ch.setAttribute('fill', color); }
if (curstroke && curstroke !== 'none') { ch.setAttribute('stroke', color); }
});
}
/**
* Called when the main system creates or modifies an object.
* Its primary purpose is to create new markers for cloned objects.
* @param {Element} el
* @returns {void}
*/
function updateReferences (el) {
$.each(mtypes, function (i, pos) {
const id = markerPrefix + pos + '_' + el.id;
const markerName = 'marker-' + pos;
const marker = getLinked(el, markerName);
if (!marker || !marker.attributes.se_type) { return; } // not created by this extension
const url = el.getAttribute(markerName);
if (url) {
const len = el.id.length;
const linkid = url.substr(-len - 1, len);
if (el.id !== linkid) {
const val = $id(pos + '_marker').getAttribute('value');
addMarker(id, val);
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
if (el.tagName === 'line' && pos === 'mid') { el = convertline(el); }
svgCanvas.call('changed', selElems);
}
}
});
}
// simulate a change event a text box that stores the current element's marker type
/**
* @param {"start"|"mid"|"end"} pos
* @param {string} val
* @returns {void}
*/
function triggerTextEntry (pos, val) {
$id(pos + '_marker').value = val;
$id(pos + '_marker').change();
}
/**
* @param {"start"|"mid"|"end"} pos
* @returns {void} Resolves to `undefined`
*/
function showTextPrompt (pos) {
let def = $id(pos + '_marker').value;
if (def.substr(0, 1) === '\\') { def = ''; }
// eslint-disable-next-line no-alert
const txt = prompt('Enter text for ' + pos + ' marker', def);
if (txt) {
triggerTextEntry(pos, txt);
}
}
/*
function setMarkerSet(obj) {
const parts = this.id.split('_');
const set = parts[2];
switch (set) {
case 'off':
triggerTextEntry('start','\\nomarker');
triggerTextEntry('mid','\\nomarker');
triggerTextEntry('end','\\nomarker');
break;
case 'dimension':
triggerTextEntry('start','\\leftarrow');
triggerTextEntry('end','\\rightarrow');
await showTextPrompt('mid');
break;
case 'label':
triggerTextEntry('mid','\\nomarker');
triggerTextEntry('end','\\rightarrow');
await showTextPrompt('start');
break;
}
}
*/
// callback function for a toolbar button click
/**
* @param {Event} ev
* @returns {Promise<void>} Resolves to `undefined`
*/
async function setArrowFromButton () {
const parts = this.id.split('_');
const pos = parts[1];
let val = parts[2];
if (parts[3]) { val += '_' + parts[3]; }
if (val !== 'textmarker') {
triggerTextEntry(pos, '\\' + val);
} else {
await showTextPrompt(pos);
}
}
/**
* @param {"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} id
* @returns {string}
*/
function getTitle (id) {
const { langList } = strings;
const item = langList.find((itm) => {
return itm.id === id;
});
return item ? item.title : id;
}
/**
* Build the toolbar button array from the marker definitions.
* @returns {module:SVGEditor.Button[]}
*/
function buildButtonList () {
const buttons = [];
// const i = 0;
/*
buttons.push({
id: idPrefix + 'markers_off',
title: 'Turn off all markers',
type: 'context',
events: { click: setMarkerSet },
panel: 'marker_panel'
});
buttons.push({
id: idPrefix + 'markers_dimension',
title: 'Dimension',
type: 'context',
events: { click: setMarkerSet },
panel: 'marker_panel'
});
buttons.push({
id: idPrefix + 'markers_label',
title: 'Label',
type: 'context',
events: { click: setMarkerSet },
panel: 'marker_panel'
});
*/
$.each(mtypes, function (k, pos) {
const listname = pos + '_marker_list';
let def = true;
Object.keys(markerTypes).forEach(function (id) {
const title = getTitle(String(id));
buttons.push({
id: idPrefix + pos + '_' + id,
svgicon: id,
icon: id + '.svg',
title,
type: 'context',
events: { click: setArrowFromButton },
panel: 'marker_panel',
list: listname,
isDefault: def
});
def = false;
});
});
return buttons;
}
const contextTools = [
{
type: 'input',
panel: 'marker_panel',
id: 'start_marker',
size: 3,
events: { change: setMarker }
}, {
type: 'button-select',
panel: 'marker_panel',
id: 'start_marker_list',
colnum: 3,
events: { change: setArrowFromButton }
}, {
type: 'input',
panel: 'marker_panel',
id: 'mid_marker',
defval: '',
size: 3,
events: { change: setMarker }
}, {
type: 'button-select',
panel: 'marker_panel',
id: 'mid_marker_list',
colnum: 3,
events: { change: setArrowFromButton }
}, {
type: 'input',
panel: 'marker_panel',
id: 'end_marker',
size: 3,
events: { change: setMarker }
}, {
type: 'button-select',
panel: 'marker_panel',
id: 'end_marker_list',
colnum: 3,
events: { change: setArrowFromButton }
}
];
return {
name: strings.name,
svgicons: '',
callback () {
if($id("marker_panel") !== null) {
$id("marker_panel").classList.add('toolset');
$id("marker_panel").style.display = 'none';
}
},
/* async */ addLangData ({ _importLocale, _lang }) {
return { data: strings.langList };
},
selectedChanged (opts) {
// Use this to update the current selected elements
selElems = opts.elems;
const markerElems = [ 'line', 'path', 'polyline', 'polygon' ];
let i = selElems.length;
while (i--) {
const elem = selElems[i];
if (elem && markerElems.includes(elem.tagName)) {
if (opts.selectedElement && !opts.multiselected) {
showPanel(true);
} else {
showPanel(false);
}
} else {
showPanel(false);
}
}
},
elementChanged (opts) {
const elem = opts.elems[0];
if (elem && (
elem.getAttribute('marker-start') ||
elem.getAttribute('marker-mid') ||
elem.getAttribute('marker-end')
)) {
colorChanged(elem);
updateReferences(elem);
}
// changing_flag = false; // Not apparently in use
},
buttons: buildButtonList(),
context_tools: strings.contextTools.map((contextTool, i) => {
return Object.assign(contextTools[i], contextTool);
})
};
}
};

View File

@ -4,7 +4,7 @@
"@babel/env", "@babel/env",
{ {
"useBuiltIns": "entry", "useBuiltIns": "entry",
"corejs": "3.18" "corejs": "3.19"
} }
] ]
] ]

View File

@ -28,7 +28,7 @@
}, },
{ {
"name": "Optimistik SAS", "name": "Optimistik SAS",
"email": "contact@optimistik.fr" "email": "contact@optimistik.io"
} }
], ],
"keywords": [ "keywords": [

View File

@ -413,7 +413,7 @@ exports[`use all parts of svg-edit > check tool_line_change_x_y_coordinate #0`]
stroke-width="5" stroke-width="5"
opacity="0.5" opacity="0.5"
x1="225" x1="225"
y1="200" y1="175"
x2="475" x2="475"
y2="425" y2="425"
id="svg_2" id="svg_2"
@ -472,7 +472,7 @@ exports[`use all parts of svg-edit > check tool_line_change_stroke_width #0`] =
stroke-width="15" stroke-width="15"
opacity="0.5" opacity="0.5"
x1="225" x1="225"
y1="200" y1="175"
x2="475" x2="475"
y2="425" y2="425"
id="svg_2" id="svg_2"
@ -530,7 +530,7 @@ exports[`use all parts of svg-edit > check tool_line_change_stoke_color #0`] = `
stroke-width="15" stroke-width="15"
opacity="0.5" opacity="0.5"
x1="225" x1="225"
y1="200" y1="175"
x2="475" x2="475"
y2="425" y2="425"
id="svg_2" id="svg_2"
@ -588,7 +588,7 @@ exports[`use all parts of svg-edit > check tool_line_align_to_page #0`] = `
stroke-width="15" stroke-width="15"
opacity="0.5" opacity="0.5"
x1="225" x1="225"
y1="200" y1="175"
x2="475" x2="475"
y2="425" y2="425"
id="svg_2" id="svg_2"

View File

@ -0,0 +1,35 @@
import {
visitAndApproveStorage
} from '../../../support/ui-test-helper.js';
// See https://github.com/SVG-Edit/svgedit/issues/660
describe('Fix issue 660', function () {
beforeEach(() => {
visitAndApproveStorage();
cy.viewport(512, 512);
});
/** @todo: reenable this test when we understand why it is passing locally but not on ci */
it.skip('can resize text', function () {
cy.get('#tool_source').click();
cy.get('#svg_source_textarea')
.type('{selectall}', { force: true })
.type(`<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<text fill="#000000" id="a_text" text-anchor="middle" x="260.5" xml:space="preserve" y="192.5" font-size="40">hello</text>
</g>
</svg>`, { force: true, parseSpecialCharSequences: false });
cy.get('#tool_source_save').click({ force: true });
cy.get('#a_text').should('exist');
cy.get('#a_text')
.trigger('mousedown', { which: 1, force: true })
.trigger('mouseup', { force: true });
cy.get('#selectorGrip_resize_s')
.trigger('mousedown', { which: 1, force: true })
.trigger('mousemove', { clientX: 0, clientY: 600 })
.trigger('mouseup', { force: true });
// svgedit use the #text text field to capture the text
cy.get('#a_text').should('have.attr', 'transform')
.and('equal', 'matrix(1 0 0 4.54639 0 -540.825)'); // Chrome 96 is matrix(1 0 0 4.17431 0 -325.367)
});
});

View File

@ -53,7 +53,7 @@ describe('Basic Module', function () {
imgPath: '../editor/images', imgPath: '../editor/images',
langPath: 'locale/', langPath: 'locale/',
extPath: 'extensions/', extPath: 'extensions/',
extensions: [ 'ext-arrows.js', 'ext-connector.js', 'ext-eyedropper.js' ], extensions: [ 'ext-arrows.js', 'ext-eyedropper.js' ],
initTool: 'select', initTool: 'select',
wireframe: false wireframe: false
} }

View File

@ -171,7 +171,7 @@ export default class ConfigObj {
* @type {string[]} * @type {string[]}
*/ */
this.defaultExtensions = [ this.defaultExtensions = [
'ext-connector', // 'ext-connector',
'ext-eyedropper', 'ext-eyedropper',
'ext-grid', 'ext-grid',
'ext-imagelib', 'ext-imagelib',

View File

@ -45,7 +45,7 @@ export class SeList extends HTMLElement {
this._shadowRoot.append(template.content.cloneNode(true)); this._shadowRoot.append(template.content.cloneNode(true));
this.$dropdown = this._shadowRoot.querySelector('elix-dropdown-list'); this.$dropdown = this._shadowRoot.querySelector('elix-dropdown-list');
this.$label = this._shadowRoot.querySelector('label'); this.$label = this._shadowRoot.querySelector('label');
this.$selction = this.$dropdown.shadowRoot.querySelector('#source').querySelector('#value'); this.$selection = this.$dropdown.shadowRoot.querySelector('#value');
this.items = this.querySelectorAll("se-list-item"); this.items = this.querySelectorAll("se-list-item");
this.imgPath = svgEditor.configObj.curConfig.imgPath; this.imgPath = svgEditor.configObj.curConfig.imgPath;
} }
@ -69,7 +69,7 @@ export class SeList extends HTMLElement {
if (oldValue === newValue) return; if (oldValue === newValue) return;
switch (name) { switch (name) {
case 'title': case 'title':
this.$dropdown.setAttribute('title', `${t(newValue)}`); this.$dropdown.setAttribute('title', t(newValue));
break; break;
case 'label': case 'label':
this.$label.textContent = t(newValue); this.$label.textContent = t(newValue);
@ -84,15 +84,17 @@ export class SeList extends HTMLElement {
Array.from(this.items).forEach(function (element) { Array.from(this.items).forEach(function (element) {
if(element.getAttribute("value") === newValue) { if(element.getAttribute("value") === newValue) {
if (element.hasAttribute("src")) { if (element.hasAttribute("src")) {
while(currentObj.$selction.firstChild) // empty current selection children
currentObj.$selction.removeChild(currentObj.$selction.firstChild); while(currentObj.$selection.firstChild)
currentObj.$selection.removeChild(currentObj.$selection.firstChild);
// replace selection child with image of new value
const img = document.createElement('img'); const img = document.createElement('img');
img.src = currentObj.imgPath + '/' + element.getAttribute("src"); img.src = currentObj.imgPath + '/' + element.getAttribute("src");
img.style.height = element.getAttribute("img-height"); img.style.height = element.getAttribute("img-height");
img.setAttribute('title', t(element.getAttribute("title"))); img.setAttribute('title', t(element.getAttribute("title")));
currentObj.$selction.append(img); currentObj.$selection.append(img);
} else { } else {
currentObj.$selction.textContent = t(element.getAttribute('option')); currentObj.$selection.textContent = t(element.getAttribute('option'));
} }
} }
}); });

View File

@ -0,0 +1,329 @@
/**
* @file ext-markers.js
*
* @license Apache-2.0
*
* @copyright 2010 Will Schleter based on ext-arrows.js by Copyright(c) 2010 Alexis Deveria
* @copyright 2021 OptimistikSAS
*
* This extension provides for the addition of markers to the either end
* or the middle of a line, polyline, path, polygon.
*
* Markers are graphics
*
* to simplify the coding and make the implementation as robust as possible,
* markers are not shared - every object has its own set of markers.
* this relationship is maintained by a naming convention between the
* ids of the markers and the ids of the object
*
* The following restrictions exist for simplicty of use and programming
* objects and their markers to have the same color
* marker size is fixed
* an application specific attribute - se_type - is added to each marker element
* to store the type of marker
*
* @todo
* remove some of the restrictions above
*
*/
export default {
name: 'markers',
async init (S) {
const svgEditor = this;
const { svgCanvas } = svgEditor;
const { $id, addSVGElementFromJson: addElem } = svgCanvas;
const mtypes = [ 'start', 'mid', 'end' ];
const markerElems = [ 'line', 'path', 'polyline', 'polygon' ];
// note - to add additional marker types add them below with a unique id
// and add the associated icon(s) to marker-icons.svg
// the geometry is normalized to a 100x100 box with the origin at lower left
// Safari did not like negative values for low left of viewBox
// remember that the coordinate system has +y downward
const markerTypes = {
nomarker: {},
leftarrow:
{ element: 'path', attr: { d: 'M0,50 L100,90 L70,50 L100,10 Z' } },
rightarrow:
{ element: 'path', attr: { d: 'M100,50 L0,90 L30,50 L0,10 Z' } },
box:
{ element: 'path', attr: { d: 'M20,20 L20,80 L80,80 L80,20 Z' } },
mcircle:
{ element: 'circle', attr: { r: 30, cx: 50, cy: 50 } }
};
// duplicate shapes to support unfilled (open) marker types with an _o suffix
[ 'leftarrow', 'rightarrow', 'box', 'mcircle' ].forEach((v) => {
markerTypes[v + '_o'] = markerTypes[v];
});
/**
* @param {Element} elem - A graphic element will have an attribute like marker-start
* @param {"marker-start"|"marker-mid"|"marker-end"} attr
* @returns {Element} The marker element that is linked to the graphic element
*/
const getLinked = (elem, attr) => {
const str = elem.getAttribute(attr);
if (!str) { return null; }
const m = str.match(/\(#(.*)\)/);
// "url(#mkr_end_svg_1)" would give m[1] = "mkr_end_svg_1"
if (!m || m.length !== 2) {
return null;
}
return svgCanvas.getElem(m[1]);
};
/**
* Toggles context tool panel off/on.
* @param {boolean} on
* @returns {void}
*/
const showPanel = (on, elem) => {
$id('marker_panel').style.display = (on) ? 'block' : 'none';
if (on && elem) {
mtypes.forEach((pos) => {
const marker = getLinked(elem, 'marker-' + pos);
if (marker?.attributes?.se_type) {
$id(`${pos}_marker_list_opts`).setAttribute('value', marker.attributes.se_type.value);
} else {
$id(`${pos}_marker_list_opts`).setAttribute('value', 'nomarker');
}
});
}
};
/**
* @param {string} id
* @param {""|"nomarker"|"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} seType
* @returns {SVGMarkerElement}
*/
const addMarker = (id, seType) => {
const selElems = svgCanvas.getSelectedElems();
let marker = svgCanvas.getElem(id);
if (marker) { return undefined; }
if (seType === '' || seType === 'nomarker') { return undefined; }
const el = selElems[0];
const color = el.getAttribute('stroke');
const strokeWidth = 10;
const refX = 50;
const refY = 50;
const viewBox = '0 0 100 100';
const markerWidth = 5;
const markerHeight = 5;
if (!markerTypes[seType]) {
console.error(`unknown marker type: ${seType}`);
return undefined;
}
// create a generic marker
marker = addElem({
element: 'marker',
attr: {
id,
markerUnits: 'strokeWidth',
orient: 'auto',
style: 'pointer-events:none',
se_type: seType
}
});
const mel = addElem(markerTypes[seType]);
const fillcolor = (seType.substr(-2) === '_o')
? 'none'
: color;
mel.setAttribute('fill', fillcolor);
mel.setAttribute('stroke', color);
mel.setAttribute('stroke-width', strokeWidth);
marker.append(mel);
marker.setAttribute('viewBox', viewBox);
marker.setAttribute('markerWidth', markerWidth);
marker.setAttribute('markerHeight', markerHeight);
marker.setAttribute('refX', refX);
marker.setAttribute('refY', refY);
svgCanvas.findDefs().append(marker);
return marker;
};
/**
* @param {Element} elem
* @returns {SVGPolylineElement}
*/
const convertline = (elem) => {
// this routine came from the connectors extension
// it is needed because midpoint markers don't work with line elements
if (elem.tagName !== 'line') { return elem; }
// Convert to polyline to accept mid-arrow
const x1 = Number(elem.getAttribute('x1'));
const x2 = Number(elem.getAttribute('x2'));
const y1 = Number(elem.getAttribute('y1'));
const y2 = Number(elem.getAttribute('y2'));
const { id } = elem;
const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' ');
const pline = addElem({
element: 'polyline',
attr: {
points: (x1 + ',' + y1 + midPt + x2 + ',' + y2),
stroke: elem.getAttribute('stroke'),
'stroke-width': elem.getAttribute('stroke-width'),
fill: 'none',
opacity: elem.getAttribute('opacity') || 1
}
});
mtypes.forEach((pos) => { // get any existing marker definitions
const nam = 'marker-' + pos;
const m = elem.getAttribute(nam);
if (m) { pline.setAttribute(nam, elem.getAttribute(nam)); }
});
const batchCmd = new S.BatchCommand();
batchCmd.addSubCommand(new S.RemoveElementCommand(elem, elem.parentNode));
batchCmd.addSubCommand(new S.InsertElementCommand(pline));
elem.insertAdjacentElement('afterend', pline);
elem.remove();
svgCanvas.clearSelection();
pline.id = id;
svgCanvas.addToSelection([ pline ]);
S.addCommandToHistory(batchCmd);
return pline;
};
/**
*
* @returns {void}
*/
const setMarker = (pos, markerType) => {
const selElems = svgCanvas.getSelectedElems();
if (selElems.length === 0) return;
const markerName = 'marker-' + pos;
const el = selElems[0];
const marker = getLinked(el, markerName);
if (marker) { marker.remove(); }
el.removeAttribute(markerName);
let val = markerType;
if (val === '') { val = 'nomarker'; }
if (val === 'nomarker') {
svgCanvas.call('changed', selElems);
return;
}
// Set marker on element
const id = 'mkr_' + pos + '_' + el.id;
addMarker(id, val);
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
if (el.tagName === 'line' && pos === 'mid') {
convertline(el);
}
svgCanvas.call('changed', selElems);
};
/**
* Called when the main system modifies an object. This routine changes
* the associated markers to be the same color.
* @param {Element} elem
* @returns {void}
*/
const colorChanged = (elem) => {
const color = elem.getAttribute('stroke');
mtypes.forEach((pos) => {
const marker = getLinked(elem, 'marker-' + pos);
if (!marker) { return; }
if (!marker.attributes.se_type) { return; } // not created by this extension
const ch = marker.lastElementChild;
if (!ch) { return; }
const curfill = ch.getAttribute('fill');
const curstroke = ch.getAttribute('stroke');
if (curfill && curfill !== 'none') { ch.setAttribute('fill', color); }
if (curstroke && curstroke !== 'none') { ch.setAttribute('stroke', color); }
});
};
/**
* Called when the main system creates or modifies an object.
* Its primary purpose is to create new markers for cloned objects.
* @param {Element} el
* @returns {void}
*/
const updateReferences = (el) => {
const selElems = svgCanvas.getSelectedElems();
mtypes.forEach((pos) => {
const markerName = 'marker-' + pos;
const marker = getLinked(el, markerName);
if (!marker || !marker.attributes.se_type) { return; } // not created by this extension
const url = el.getAttribute(markerName);
if (url) {
const len = el.id.length;
const linkid = url.substr(-len - 1, len);
if (el.id !== linkid) {
const newMarkerId = 'mkr_' + pos + '_' + el.id;
addMarker(newMarkerId, marker.attributes.se_type.value);
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + newMarkerId + ')');
svgCanvas.call('changed', selElems);
}
}
});
};
return {
name: svgEditor.i18next.t(`${name}:name`),
// The callback should be used to load the DOM with the appropriate UI items
callback() {
// Add the context panel and its handler(s)
const panelTemplate = document.createElement("template");
// create the marker panel
let innerHTML = '<div id="marker_panel">';
mtypes.forEach((pos) => {
innerHTML += `<se-list id="${pos}_marker_list_opts" title="tools.${pos}_marker_list_opts" label="" width="22px" height="22px">`;
Object.entries(markerTypes).forEach(([ marker, _mkr ]) => {
innerHTML += `<se-list-item id="mkr_${pos}_${marker}" value="${marker}" title="tools.mkr_${marker}" src="${marker}.svg" img-height="22px"></se-list-item>`;
});
innerHTML += '</se-list>';
});
innerHTML += '</div>';
// eslint-disable-next-line no-unsanitized/property
panelTemplate.innerHTML = innerHTML;
$id("tools_top").appendChild(panelTemplate.content.cloneNode(true));
// don't display the panels on start
showPanel(false);
mtypes.forEach((pos) => {
$id(`${pos}_marker_list_opts`).addEventListener('change', (evt) => {
setMarker(pos, evt.detail.value);
});
});
},
selectedChanged (opts) {
// Use this to update the current selected elements
if (opts.elems.length === 0) showPanel(false);
opts.elems.forEach( (elem) => {
if (elem && markerElems.includes(elem.tagName)) {
if (opts.selectedElement && !opts.multiselected) {
showPanel(true, elem);
} else {
showPanel(false);
}
} else {
showPanel(false);
}
});
},
elementChanged (opts) {
const elem = opts.elems[0];
if (elem && (
elem.getAttribute('marker-start') ||
elem.getAttribute('marker-mid') ||
elem.getAttribute('marker-end')
)) {
colorChanged(elem);
updateReferences(elem);
}
}
};
}
};

View File

@ -449,9 +449,6 @@ export default {
showPanel(false, "polygon"); showPanel(false, "polygon");
} }
} }
},
elementChanged(_opts) {
// const elem = opts.elems[0];
} }
}; };
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 B

View File

@ -1046,6 +1046,7 @@ class TopPanel {
"rect_height", "rect_height",
"line_x1", "line_x1",
"line_x2", "line_x2",
"line_y1",
"line_y2", "line_y2",
"image_width", "image_width",
"image_height", "image_height",

View File

@ -42,7 +42,7 @@ const svgWhiteList_ = {
image: [ 'clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'systemLanguage', 'width', 'x', 'xlink:href', 'xlink:title', 'y' ], image: [ 'clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'systemLanguage', 'width', 'x', 'xlink:href', 'xlink:title', 'y' ],
line: [ 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'x1', 'x2', 'y1', 'y2' ], line: [ 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'x1', 'x2', 'y1', 'y2' ],
linearGradient: [ 'gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2' ], linearGradient: [ 'gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2' ],
marker: [ 'markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'systemLanguage', 'viewBox' ], marker: [ 'markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'se_type', 'systemLanguage', 'viewBox' ],
mask: [ 'height', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y' ], mask: [ 'height', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y' ],
metadata: [ ], metadata: [ ],
path: [ 'clip-path', 'clip-rule', 'd', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage' ], path: [ 'clip-path', 'clip-rule', 'd', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage' ],