diff --git a/.eslintrc.js b/.eslintrc.js index 66ca2db9..e6096b1b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -63,7 +63,9 @@ module.exports = { rules: { // with ci, instrumented is not created before linter "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 } }, { diff --git a/.npmignore b/.npmignore index 74afe2fc..dbd7ab3e 100644 --- a/.npmignore +++ b/.npmignore @@ -1,19 +1,6 @@ -ignore -screencasts - -.github/ISSUE_TEMPLATE/bug_report.md -gh-disabled-workflows -build lgtm.yml - -cypress/** -cypress.env.json - coverage/** .nyc_output instrumented/** - -releases - -tools .eslintcache +node_modules/** diff --git a/.remarkrc b/.remarkrc deleted file mode 100644 index da24a20e..00000000 --- a/.remarkrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": { - "lint-ordered-list-marker-value": "one" - } -} diff --git a/AUTHORS b/AUTHORS index 28d0ba8b..873359f0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,6 +4,7 @@ Jeff Schiller Vidar Hokstad Alexis Deveria Brett Zamir +Optimistik SAS Translation credits: diff --git a/CHANGES.md b/CHANGES.md index 72ace049..c9b2e8a9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,11 @@ # SVG-Edit CHANGES -## 7.0.0 (preview - work in progress) +## 7.0.0 - New UI - Rearchitecture the code (more modular) -- simplify and refresh the build process +- Simplify and refresh the build process - Introduce Web Component to replace jQuery UI -- update dependencies +- Update dependencies ## 6.0.0 (unreleased) - Project: Add `FUNDING.yml` to accept contributions diff --git a/README.md b/README.md index fa70415c..6625e1fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# SVG-Edit +# 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) @@ -24,19 +24,19 @@ works in any modern browser. ![screenshot](docs/screenshot.png) [](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. -If you can help us to maintain SVG-Edit, you are more than welcome! +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. +Please let us know with an issue or a discussions if you wish to contribute. ## 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 @@ -47,33 +47,20 @@ Thanks to Netlify, you can test the following builds: 1. run `npm run start` to start a local server 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: 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.) -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 - - - Opera 59+, - - 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. - + 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). + 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 * 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. * [StackOverflow](https://stackoverflow.com/tags/svg-edit) group. diff --git a/src/editor/extensions/ext-connector/ext-connector.js b/archive/untested-extensions/ext-connector/ext-connector.js similarity index 100% rename from src/editor/extensions/ext-connector/ext-connector.js rename to archive/untested-extensions/ext-connector/ext-connector.js diff --git a/src/editor/extensions/ext-connector/locale/en.js b/archive/untested-extensions/ext-connector/locale/en.js similarity index 100% rename from src/editor/extensions/ext-connector/locale/en.js rename to archive/untested-extensions/ext-connector/locale/en.js diff --git a/src/editor/extensions/ext-connector/locale/fr.js b/archive/untested-extensions/ext-connector/locale/fr.js similarity index 100% rename from src/editor/extensions/ext-connector/locale/fr.js rename to archive/untested-extensions/ext-connector/locale/fr.js diff --git a/src/editor/extensions/ext-connector/locale/zh-CN.js b/archive/untested-extensions/ext-connector/locale/zh-CN.js similarity index 100% rename from src/editor/extensions/ext-connector/locale/zh-CN.js rename to archive/untested-extensions/ext-connector/locale/zh-CN.js diff --git a/archive/untested-extensions/ext-markers/ext-markers.js b/archive/untested-extensions/ext-markers/ext-markers.js deleted file mode 100644 index cb324f44..00000000 --- a/archive/untested-extensions/ext-markers/ext-markers.js +++ /dev/null @@ -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(/\(#(?.+)\)/); - // 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} 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); - }) - }; - } -}; diff --git a/archive/extensions/ext-opensave/ext-opensave.js b/archive/untested-extensions/extensions/ext-opensave/ext-opensave.js similarity index 100% rename from archive/extensions/ext-opensave/ext-opensave.js rename to archive/untested-extensions/extensions/ext-opensave/ext-opensave.js diff --git a/babel.config.json b/babel.config.json index 6cd97a0e..a5a181fc 100644 --- a/babel.config.json +++ b/babel.config.json @@ -4,7 +4,7 @@ "@babel/env", { "useBuiltIns": "entry", - "corejs": "3.18" + "corejs": "3.19" } ] ] diff --git a/composer.json b/composer.json index f2c968bd..d30c0e6e 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ }, { "name": "Optimistik SAS", - "email": "contact@optimistik.fr" + "email": "contact@optimistik.io" } ], "keywords": [ diff --git a/cypress/integration/ui/__snapshots__/scenario5.js.snap b/cypress/integration/ui/__snapshots__/scenario5.js.snap index 2f0869fa..1310702f 100644 --- a/cypress/integration/ui/__snapshots__/scenario5.js.snap +++ b/cypress/integration/ui/__snapshots__/scenario5.js.snap @@ -413,7 +413,7 @@ exports[`use all parts of svg-edit > check tool_line_change_x_y_coordinate #0`] stroke-width="5" opacity="0.5" x1="225" - y1="200" + y1="175" x2="475" y2="425" id="svg_2" @@ -472,7 +472,7 @@ exports[`use all parts of svg-edit > check tool_line_change_stroke_width #0`] = stroke-width="15" opacity="0.5" x1="225" - y1="200" + y1="175" x2="475" y2="425" id="svg_2" @@ -530,7 +530,7 @@ exports[`use all parts of svg-edit > check tool_line_change_stoke_color #0`] = ` stroke-width="15" opacity="0.5" x1="225" - y1="200" + y1="175" x2="475" y2="425" id="svg_2" @@ -588,7 +588,7 @@ exports[`use all parts of svg-edit > check tool_line_align_to_page #0`] = ` stroke-width="15" opacity="0.5" x1="225" - y1="200" + y1="175" x2="475" y2="425" id="svg_2" diff --git a/cypress/integration/ui/issues/issue-660.js b/cypress/integration/ui/issues/issue-660.js new file mode 100644 index 00000000..3eeefc21 --- /dev/null +++ b/cypress/integration/ui/issues/issue-660.js @@ -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(` + + Layer 1 + hello + + `, { 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) + }); +}); diff --git a/cypress/integration/unit/test1.js b/cypress/integration/unit/test1.js index a61fdd5a..cba8bf26 100644 --- a/cypress/integration/unit/test1.js +++ b/cypress/integration/unit/test1.js @@ -53,7 +53,7 @@ describe('Basic Module', function () { imgPath: '../editor/images', langPath: 'locale/', extPath: 'extensions/', - extensions: [ 'ext-arrows.js', 'ext-connector.js', 'ext-eyedropper.js' ], + extensions: [ 'ext-arrows.js', 'ext-eyedropper.js' ], initTool: 'select', wireframe: false } diff --git a/src/editor/ConfigObj.js b/src/editor/ConfigObj.js index 24dd4cc1..f5400d78 100644 --- a/src/editor/ConfigObj.js +++ b/src/editor/ConfigObj.js @@ -171,7 +171,7 @@ export default class ConfigObj { * @type {string[]} */ this.defaultExtensions = [ - 'ext-connector', + // 'ext-connector', 'ext-eyedropper', 'ext-grid', 'ext-imagelib', diff --git a/src/editor/components/seList.js b/src/editor/components/seList.js index db63e9a2..06cb7cb4 100644 --- a/src/editor/components/seList.js +++ b/src/editor/components/seList.js @@ -45,7 +45,7 @@ export class SeList extends HTMLElement { this._shadowRoot.append(template.content.cloneNode(true)); this.$dropdown = this._shadowRoot.querySelector('elix-dropdown-list'); 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.imgPath = svgEditor.configObj.curConfig.imgPath; } @@ -69,7 +69,7 @@ export class SeList extends HTMLElement { if (oldValue === newValue) return; switch (name) { case 'title': - this.$dropdown.setAttribute('title', `${t(newValue)}`); + this.$dropdown.setAttribute('title', t(newValue)); break; case 'label': this.$label.textContent = t(newValue); @@ -84,15 +84,17 @@ export class SeList extends HTMLElement { Array.from(this.items).forEach(function (element) { if(element.getAttribute("value") === newValue) { if (element.hasAttribute("src")) { - while(currentObj.$selction.firstChild) - currentObj.$selction.removeChild(currentObj.$selction.firstChild); + // empty current selection children + while(currentObj.$selection.firstChild) + currentObj.$selection.removeChild(currentObj.$selection.firstChild); + // replace selection child with image of new value const img = document.createElement('img'); img.src = currentObj.imgPath + '/' + element.getAttribute("src"); img.style.height = element.getAttribute("img-height"); img.setAttribute('title', t(element.getAttribute("title"))); - currentObj.$selction.append(img); + currentObj.$selection.append(img); } else { - currentObj.$selction.textContent = t(element.getAttribute('option')); + currentObj.$selection.textContent = t(element.getAttribute('option')); } } }); diff --git a/src/editor/extensions/ext-markers/ext-markers.js b/src/editor/extensions/ext-markers/ext-markers.js new file mode 100644 index 00000000..d1c0592b --- /dev/null +++ b/src/editor/extensions/ext-markers/ext-markers.js @@ -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 = '
'; + mtypes.forEach((pos) => { + innerHTML += ``; + Object.entries(markerTypes).forEach(([ marker, _mkr ]) => { + innerHTML += ``; + }); + innerHTML += ''; + }); + innerHTML += '
'; + // 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); + } + } + }; + } +}; diff --git a/archive/untested-extensions/ext-markers/locale/en.js b/src/editor/extensions/ext-markers/locale/en.js similarity index 100% rename from archive/untested-extensions/ext-markers/locale/en.js rename to src/editor/extensions/ext-markers/locale/en.js diff --git a/archive/untested-extensions/ext-markers/locale/zh-CN.js b/src/editor/extensions/ext-markers/locale/zh-CN.js similarity index 100% rename from archive/untested-extensions/ext-markers/locale/zh-CN.js rename to src/editor/extensions/ext-markers/locale/zh-CN.js diff --git a/src/editor/extensions/ext-polystar/ext-polystar.js b/src/editor/extensions/ext-polystar/ext-polystar.js index b93f909e..cee59068 100644 --- a/src/editor/extensions/ext-polystar/ext-polystar.js +++ b/src/editor/extensions/ext-polystar/ext-polystar.js @@ -449,9 +449,6 @@ export default { showPanel(false, "polygon"); } } - }, - elementChanged(_opts) { - // const elem = opts.elems[0]; } }; } diff --git a/src/editor/images/sep.png b/src/editor/images/sep.png deleted file mode 100644 index 5485bc48..00000000 Binary files a/src/editor/images/sep.png and /dev/null differ diff --git a/src/editor/panels/TopPanel.js b/src/editor/panels/TopPanel.js index 910087f2..4b160f47 100644 --- a/src/editor/panels/TopPanel.js +++ b/src/editor/panels/TopPanel.js @@ -1046,6 +1046,7 @@ class TopPanel { "rect_height", "line_x1", "line_x2", + "line_y1", "line_y2", "image_width", "image_height", diff --git a/src/svgcanvas/sanitize.js b/src/svgcanvas/sanitize.js index 8544ca4f..b24402e1 100644 --- a/src/svgcanvas/sanitize.js +++ b/src/svgcanvas/sanitize.js @@ -42,7 +42,7 @@ const svgWhiteList_ = { 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' ], 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' ], 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' ],