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