| /* globals jQuery */ | 
| /** | 
|  * ext-connector.js | 
|  * | 
|  * @license MIT | 
|  * | 
|  * @copyright 2010 Alexis Deveria | 
|  * | 
|  */ | 
|   | 
| export default { | 
|   name: 'connector', | 
|   async init (S) { | 
|     const $ = jQuery; | 
|     const svgEditor = this; | 
|     const svgCanvas = svgEditor.canvas; | 
|     const {getElem} = svgCanvas; | 
|     const {svgroot, importLocale} = S, | 
|       addElem = svgCanvas.addSVGElementFromJson, | 
|       selManager = S.selectorManager, | 
|       connSel = '.se_connector', | 
|       // connect_str = '-SE_CONNECT-', | 
|       elData = $.data; | 
|     const strings = await importLocale(); | 
|   | 
|     let startX, | 
|       startY, | 
|       curLine, | 
|       startElem, | 
|       endElem, | 
|       seNs, | 
|       {svgcontent} = S, | 
|       started = false, | 
|       connections = [], | 
|       selElems = []; | 
|   | 
|     function getBBintersect (x, y, bb, offset) { | 
|       if (offset) { | 
|         offset -= 0; | 
|         bb = $.extend({}, bb); | 
|         bb.width += offset; | 
|         bb.height += offset; | 
|         bb.x -= offset / 2; | 
|         bb.y -= offset / 2; | 
|       } | 
|   | 
|       const midX = bb.x + bb.width / 2; | 
|       const midY = bb.y + bb.height / 2; | 
|       const lenX = x - midX; | 
|       const lenY = y - midY; | 
|   | 
|       const slope = Math.abs(lenY / lenX); | 
|   | 
|       let ratio; | 
|       if (slope < bb.height / bb.width) { | 
|         ratio = (bb.width / 2) / Math.abs(lenX); | 
|       } else { | 
|         ratio = lenY | 
|           ? (bb.height / 2) / Math.abs(lenY) | 
|           : 0; | 
|       } | 
|   | 
|       return { | 
|         x: midX + lenX * ratio, | 
|         y: midY + lenY * ratio | 
|       }; | 
|     } | 
|   | 
|     function getOffset (side, line) { | 
|       const giveOffset = !!line.getAttribute('marker-' + side); | 
|       // const giveOffset = $(line).data(side+'_off'); | 
|   | 
|       // TODO: Make this number (5) be based on marker width/height | 
|       const size = line.getAttribute('stroke-width') * 5; | 
|       return giveOffset ? size : 0; | 
|     } | 
|   | 
|     function showPanel (on) { | 
|       let connRules = $('#connector_rules'); | 
|       if (!connRules.length) { | 
|         connRules = $('<style id="connector_rules"></style>').appendTo('head'); | 
|       } | 
|       connRules.text(!on ? '' : '#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }'); | 
|       $('#connector_panel').toggle(on); | 
|     } | 
|   | 
|     function setPoint (elem, pos, x, y, setMid) { | 
|       const pts = elem.points; | 
|       const pt = svgroot.createSVGPoint(); | 
|       pt.x = x; | 
|       pt.y = y; | 
|       if (pos === 'end') { pos = pts.numberOfItems - 1; } | 
|       // TODO: Test for this on init, then use alt only if needed | 
|       try { | 
|         pts.replaceItem(pt, pos); | 
|       } catch (err) { | 
|         // Should only occur in FF which formats points attr as "n,n n,n", so just split | 
|         const ptArr = elem.getAttribute('points').split(' '); | 
|         for (let i = 0; i < ptArr.length; i++) { | 
|           if (i === pos) { | 
|             ptArr[i] = x + ',' + y; | 
|           } | 
|         } | 
|         elem.setAttribute('points', ptArr.join(' ')); | 
|       } | 
|   | 
|       if (setMid) { | 
|         // Add center point | 
|         const ptStart = pts.getItem(0); | 
|         const ptEnd = pts.getItem(pts.numberOfItems - 1); | 
|         setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2); | 
|       } | 
|     } | 
|   | 
|     function updateLine (diffX, diffY) { | 
|       // Update line with element | 
|       let i = connections.length; | 
|       while (i--) { | 
|         const conn = connections[i]; | 
|         const line = conn.connector; | 
|         // const {elem} = conn; | 
|   | 
|         const pre = conn.is_start ? 'start' : 'end'; | 
|         // const sw = line.getAttribute('stroke-width') * 5; | 
|   | 
|         // Update bbox for this element | 
|         const bb = elData(line, pre + '_bb'); | 
|         bb.x = conn.start_x + diffX; | 
|         bb.y = conn.start_y + diffY; | 
|         elData(line, pre + '_bb', bb); | 
|   | 
|         const altPre = conn.is_start ? 'end' : 'start'; | 
|   | 
|         // Get center pt of connected element | 
|         const bb2 = elData(line, altPre + '_bb'); | 
|         const srcX = bb2.x + bb2.width / 2; | 
|         const srcY = bb2.y + bb2.height / 2; | 
|   | 
|         // Set point of element being moved | 
|         const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 | 
|         setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); | 
|   | 
|         // Set point of connected element | 
|         const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); | 
|         setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); | 
|       } | 
|     } | 
|   | 
|     /** | 
|     * | 
|     * @param {Element[]} [elems=selElems] Array of elements | 
|     * @returns {undefined} | 
|     */ | 
|     function findConnectors (elems = selElems) { | 
|       const connectors = $(svgcontent).find(connSel); | 
|       connections = []; | 
|   | 
|       // Loop through connectors to see if one is connected to the element | 
|       connectors.each(function () { | 
|         let addThis; | 
|         function add () { | 
|           if (elems.includes(this)) { | 
|             // Pretend this element is selected | 
|             addThis = true; | 
|           } | 
|         } | 
|   | 
|         // Grab the ends | 
|         const parts = []; | 
|         ['start', 'end'].forEach(function (pos, i) { | 
|           const key = 'c_' + pos; | 
|           let part = elData(this, key); | 
|           if (part == null) { | 
|             part = document.getElementById( | 
|               this.attributes['se:connector'].value.split(' ')[i] | 
|             ); | 
|             elData(this, 'c_' + pos, part.id); | 
|             elData(this, pos + '_bb', svgCanvas.getStrokedBBox([part])); | 
|           } else part = document.getElementById(part); | 
|           parts.push(part); | 
|         }.bind(this)); | 
|   | 
|         for (let i = 0; i < 2; i++) { | 
|           const cElem = parts[i]; | 
|   | 
|           addThis = false; | 
|           // The connected element might be part of a selected group | 
|           $(cElem).parents().each(add); | 
|   | 
|           if (!cElem || !cElem.parentNode) { | 
|             $(this).remove(); | 
|             continue; | 
|           } | 
|           if (elems.includes(cElem) || addThis) { | 
|             const bb = svgCanvas.getStrokedBBox([cElem]); | 
|             connections.push({ | 
|               elem: cElem, | 
|               connector: this, | 
|               is_start: (i === 0), | 
|               start_x: bb.x, | 
|               start_y: bb.y | 
|             }); | 
|           } | 
|         } | 
|       }); | 
|     } | 
|   | 
|     function updateConnectors (elems) { | 
|       // Updates connector lines based on selected elements | 
|       // Is not used on mousemove, as it runs getStrokedBBox every time, | 
|       // which isn't necessary there. | 
|       findConnectors(elems); | 
|       if (connections.length) { | 
|         // Update line with element | 
|         let i = connections.length; | 
|         while (i--) { | 
|           const conn = connections[i]; | 
|           const line = conn.connector; | 
|           const {elem} = conn; | 
|   | 
|           // const sw = line.getAttribute('stroke-width') * 5; | 
|           const pre = conn.is_start ? 'start' : 'end'; | 
|   | 
|           // Update bbox for this element | 
|           const bb = svgCanvas.getStrokedBBox([elem]); | 
|           bb.x = conn.start_x; | 
|           bb.y = conn.start_y; | 
|           elData(line, pre + '_bb', bb); | 
|           /* const addOffset = */ elData(line, pre + '_off'); | 
|   | 
|           const altPre = conn.is_start ? 'end' : 'start'; | 
|   | 
|           // Get center pt of connected element | 
|           const bb2 = elData(line, altPre + '_bb'); | 
|           const srcX = bb2.x + bb2.width / 2; | 
|           const srcY = bb2.y + bb2.height / 2; | 
|   | 
|           // Set point of element being moved | 
|           let pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); | 
|           setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); | 
|   | 
|           // Set point of connected element | 
|           const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); | 
|           setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); | 
|   | 
|           // Update points attribute manually for webkit | 
|           if (navigator.userAgent.includes('AppleWebKit')) { | 
|             const pts = line.points; | 
|             const len = pts.numberOfItems; | 
|             const ptArr = []; | 
|             for (let j = 0; j < len; j++) { | 
|               pt = pts.getItem(j); | 
|               ptArr[j] = pt.x + ',' + pt.y; | 
|             } | 
|             line.setAttribute('points', ptArr.join(' ')); | 
|           } | 
|         } | 
|       } | 
|     } | 
|   | 
|     // Do once | 
|     (function () { | 
|       const gse = svgCanvas.groupSelectedElements; | 
|   | 
|       svgCanvas.groupSelectedElements = function () { | 
|         svgCanvas.removeFromSelection($(connSel).toArray()); | 
|         return gse.apply(this, arguments); | 
|       }; | 
|   | 
|       const mse = svgCanvas.moveSelectedElements; | 
|   | 
|       svgCanvas.moveSelectedElements = function () { | 
|         const cmd = mse.apply(this, arguments); | 
|         updateConnectors(); | 
|         return cmd; | 
|       }; | 
|   | 
|       seNs = svgCanvas.getEditorNS(); | 
|     }()); | 
|   | 
|     // Do on reset | 
|     function init () { | 
|       // Make sure all connectors have data set | 
|       $(svgcontent).find('*').each(function () { | 
|         const conn = this.getAttributeNS(seNs, 'connector'); | 
|         if (conn) { | 
|           this.setAttribute('class', connSel.substr(1)); | 
|           const connData = conn.split(' '); | 
|           const sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]); | 
|           const ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]); | 
|           $(this).data('c_start', connData[0]) | 
|             .data('c_end', connData[1]) | 
|             .data('start_bb', sbb) | 
|             .data('end_bb', ebb); | 
|           svgCanvas.getEditorNS(true); | 
|         } | 
|       }); | 
|       // updateConnectors(); | 
|     } | 
|   | 
|     // $(svgroot).parent().mousemove(function (e) { | 
|     // // if (started | 
|     // //   || svgCanvas.getMode() !== 'connector' | 
|     // //  || e.target.parentNode.parentNode !== svgcontent) return; | 
|     // | 
|     // console.log('y') | 
|     // // if (e.target.parentNode.parentNode === svgcontent) { | 
|     // // | 
|     // // } | 
|     // }); | 
|   | 
|     const buttons = [{ | 
|       id: 'mode_connect', | 
|       type: 'mode', | 
|       icon: svgEditor.curConfig.imgPath + 'cut.png', | 
|       includeWith: { | 
|         button: '#tool_line', | 
|         isDefault: false, | 
|         position: 1 | 
|       }, | 
|       events: { | 
|         click () { | 
|           svgCanvas.setMode('connector'); | 
|         } | 
|       } | 
|     }]; | 
|   | 
|     return { | 
|       name: strings.name, | 
|       svgicons: svgEditor.curConfig.imgPath + 'conn.svg', | 
|       buttons: strings.buttons.map((button, i) => { | 
|         return Object.assign(buttons[i], button); | 
|       }), | 
|       async addLangData ({lang, importLocale}) { | 
|         return { | 
|           data: strings.langList | 
|         }; | 
|       }, | 
|       mouseDown (opts) { | 
|         const e = opts.event; | 
|         startX = opts.start_x; | 
|         startY = opts.start_y; | 
|         const mode = svgCanvas.getMode(); | 
|         const {curConfig: {initStroke}} = svgEditor; | 
|   | 
|         if (mode === 'connector') { | 
|           if (started) { return; } | 
|   | 
|           const mouseTarget = e.target; | 
|   | 
|           const parents = $(mouseTarget).parents(); | 
|   | 
|           if ($.inArray(svgcontent, parents) !== -1) { | 
|             // Connectable element | 
|   | 
|             // If child of foreignObject, use parent | 
|             const fo = $(mouseTarget).closest('foreignObject'); | 
|             startElem = fo.length ? fo[0] : mouseTarget; | 
|   | 
|             // Get center of source element | 
|             const bb = svgCanvas.getStrokedBBox([startElem]); | 
|             const x = bb.x + bb.width / 2; | 
|             const y = bb.y + bb.height / 2; | 
|   | 
|             started = true; | 
|             curLine = addElem({ | 
|               element: 'polyline', | 
|               attr: { | 
|                 id: svgCanvas.getNextId(), | 
|                 points: (x + ',' + y + ' ' + x + ',' + y + ' ' + startX + ',' + startY), | 
|                 stroke: '#' + initStroke.color, | 
|                 'stroke-width': (!startElem.stroke_width || startElem.stroke_width === 0) | 
|                   ? initStroke.width | 
|                   : startElem.stroke_width, | 
|                 fill: 'none', | 
|                 opacity: initStroke.opacity, | 
|                 style: 'pointer-events:none' | 
|               } | 
|             }); | 
|             elData(curLine, 'start_bb', bb); | 
|           } | 
|           return { | 
|             started: true | 
|           }; | 
|         } | 
|         if (mode === 'select') { | 
|           findConnectors(); | 
|         } | 
|       }, | 
|       mouseMove (opts) { | 
|         const zoom = svgCanvas.getZoom(); | 
|         // const e = opts.event; | 
|         const x = opts.mouse_x / zoom; | 
|         const y = opts.mouse_y / zoom; | 
|   | 
|         const diffX = x - startX, | 
|           diffY = y - startY; | 
|   | 
|         const mode = svgCanvas.getMode(); | 
|   | 
|         if (mode === 'connector' && started) { | 
|           // const sw = curLine.getAttribute('stroke-width') * 3; | 
|           // Set start point (adjusts based on bb) | 
|           const pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine)); | 
|           startX = pt.x; | 
|           startY = pt.y; | 
|   | 
|           setPoint(curLine, 0, pt.x, pt.y, true); | 
|   | 
|           // Set end point | 
|           setPoint(curLine, 'end', x, y, true); | 
|         } else if (mode === 'select') { | 
|           let slen = selElems.length; | 
|           while (slen--) { | 
|             const elem = selElems[slen]; | 
|             // Look for selected connector elements | 
|             if (elem && elData(elem, 'c_start')) { | 
|               // Remove the "translate" transform given to move | 
|               svgCanvas.removeFromSelection([elem]); | 
|               svgCanvas.getTransformList(elem).clear(); | 
|             } | 
|           } | 
|           if (connections.length) { | 
|             updateLine(diffX, diffY); | 
|           } | 
|         } | 
|       }, | 
|       mouseUp (opts) { | 
|         // const zoom = svgCanvas.getZoom(); | 
|         const e = opts.event; | 
|         // , x = opts.mouse_x / zoom, | 
|         // , y = opts.mouse_y / zoom, | 
|         let mouseTarget = e.target; | 
|   | 
|         if (svgCanvas.getMode() !== 'connector') { | 
|           return; | 
|         } | 
|         const fo = $(mouseTarget).closest('foreignObject'); | 
|         if (fo.length) { mouseTarget = fo[0]; } | 
|   | 
|         const parents = $(mouseTarget).parents(); | 
|   | 
|         if (mouseTarget === startElem) { | 
|           // Start line through click | 
|           started = true; | 
|           return { | 
|             keep: true, | 
|             element: null, | 
|             started | 
|           }; | 
|         } | 
|         if ($.inArray(svgcontent, parents) === -1) { | 
|           // Not a valid target element, so remove line | 
|           $(curLine).remove(); | 
|           started = false; | 
|           return { | 
|             keep: false, | 
|             element: null, | 
|             started | 
|           }; | 
|         } | 
|         // Valid end element | 
|         endElem = mouseTarget; | 
|   | 
|         const startId = startElem.id, endId = endElem.id; | 
|         const connStr = startId + ' ' + endId; | 
|         const altStr = endId + ' ' + startId; | 
|         // Don't create connector if one already exists | 
|         const dupe = $(svgcontent).find(connSel).filter(function () { | 
|           const conn = this.getAttributeNS(seNs, 'connector'); | 
|           if (conn === connStr || conn === altStr) { return true; } | 
|         }); | 
|         if (dupe.length) { | 
|           $(curLine).remove(); | 
|           return { | 
|             keep: false, | 
|             element: null, | 
|             started: false | 
|           }; | 
|         } | 
|   | 
|         const bb = svgCanvas.getStrokedBBox([endElem]); | 
|   | 
|         const pt = getBBintersect(startX, startY, bb, getOffset('start', curLine)); | 
|         setPoint(curLine, 'end', pt.x, pt.y, true); | 
|         $(curLine) | 
|           .data('c_start', startId) | 
|           .data('c_end', endId) | 
|           .data('end_bb', bb); | 
|         seNs = svgCanvas.getEditorNS(true); | 
|         curLine.setAttributeNS(seNs, 'se:connector', connStr); | 
|         curLine.setAttribute('class', connSel.substr(1)); | 
|         curLine.setAttribute('opacity', 1); | 
|         svgCanvas.addToSelection([curLine]); | 
|         svgCanvas.moveToBottomSelectedElement(); | 
|         selManager.requestSelector(curLine).showGrips(false); | 
|         started = false; | 
|         return { | 
|           keep: true, | 
|           element: curLine, | 
|           started | 
|         }; | 
|       }, | 
|       selectedChanged (opts) { | 
|         // TODO: Find better way to skip operations if no connectors are in use | 
|         if (!$(svgcontent).find(connSel).length) { return; } | 
|   | 
|         if (svgCanvas.getMode() === 'connector') { | 
|           svgCanvas.setMode('select'); | 
|         } | 
|   | 
|         // Use this to update the current selected elements | 
|         selElems = opts.elems; | 
|   | 
|         let i = selElems.length; | 
|         while (i--) { | 
|           const elem = selElems[i]; | 
|           if (elem && elData(elem, 'c_start')) { | 
|             selManager.requestSelector(elem).showGrips(false); | 
|             if (opts.selectedElement && !opts.multiselected) { | 
|               // TODO: Set up context tools and hide most regular line tools | 
|               showPanel(true); | 
|             } else { | 
|               showPanel(false); | 
|             } | 
|           } else { | 
|             showPanel(false); | 
|           } | 
|         } | 
|         updateConnectors(); | 
|       }, | 
|       elementChanged (opts) { | 
|         let elem = opts.elems[0]; | 
|         if (elem && elem.tagName === 'svg' && elem.id === 'svgcontent') { | 
|           // Update svgcontent (can change on import) | 
|           svgcontent = elem; | 
|           init(); | 
|         } | 
|   | 
|         // Has marker, so change offset | 
|         if (elem && ( | 
|           elem.getAttribute('marker-start') || | 
|           elem.getAttribute('marker-mid') || | 
|           elem.getAttribute('marker-end') | 
|         )) { | 
|           const start = elem.getAttribute('marker-start'); | 
|           const mid = elem.getAttribute('marker-mid'); | 
|           const end = elem.getAttribute('marker-end'); | 
|           curLine = elem; | 
|           $(elem) | 
|             .data('start_off', !!start) | 
|             .data('end_off', !!end); | 
|   | 
|           if (elem.tagName === 'line' && mid) { | 
|             // 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'), | 
|                 'marker-mid': mid, | 
|                 fill: 'none', | 
|                 opacity: elem.getAttribute('opacity') || 1 | 
|               } | 
|             }); | 
|             $(elem).after(pline).remove(); | 
|             svgCanvas.clearSelection(); | 
|             pline.id = id; | 
|             svgCanvas.addToSelection([pline]); | 
|             elem = pline; | 
|           } | 
|         } | 
|         // Update line if it's a connector | 
|         if (elem.getAttribute('class') === connSel.substr(1)) { | 
|           const start = getElem(elData(elem, 'c_start')); | 
|           updateConnectors([start]); | 
|         } else { | 
|           updateConnectors(); | 
|         } | 
|       }, | 
|       IDsUpdated (input) { | 
|         const remove = []; | 
|         input.elems.forEach(function (elem) { | 
|           if ('se:connector' in elem.attr) { | 
|             elem.attr['se:connector'] = elem.attr['se:connector'].split(' ') | 
|               .map(function (oldID) { return input.changes[oldID]; }).join(' '); | 
|   | 
|             // Check validity - the field would be something like 'svg_21 svg_22', but | 
|             // if one end is missing, it would be 'svg_21' and therefore fail this test | 
|             if (!/. ./.test(elem.attr['se:connector'])) { | 
|               remove.push(elem.attr.id); | 
|             } | 
|           } | 
|         }); | 
|         return {remove}; | 
|       }, | 
|       toolButtonStateUpdate (opts) { | 
|         if (opts.nostroke) { | 
|           if ($('#mode_connect').hasClass('tool_button_current')) { | 
|             svgEditor.clickSelect(); | 
|           } | 
|         } | 
|         $('#mode_connect') | 
|           .toggleClass('disabled', opts.nostroke); | 
|       } | 
|     }; | 
|   } | 
| }; |