UNPKG

svgedit

Version:

Powerful SVG-Editor for your browser

330 lines (307 loc) 11.7 kB
/** * @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); } } }; } };