svgedit
Version:
Powerful SVG-Editor for your browser
385 lines (343 loc) • 19.9 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: extensions/ext-markers/ext-markers.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: extensions/ext-markers/ext-markers.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @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 () {
const svgEditor = this
const { svgCanvas } = svgEditor
const { BatchCommand, RemoveElementCommand, InsertElementCommand } = svgCanvas.history
const { $id, addSVGElementsFromJson: 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.getElement(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.getSelectedElements()
let marker = svgCanvas.getElement(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 BatchCommand()
batchCmd.addSubCommand(new RemoveElementCommand(elem, elem.parentNode))
batchCmd.addSubCommand(new InsertElementCommand(pline))
elem.insertAdjacentElement('afterend', pline)
elem.remove()
svgCanvas.clearSelection()
pline.id = id
svgCanvas.addToSelection([pline])
svgCanvas.addCommandToHistory(batchCmd)
return pline
}
/**
*
* @returns {void}
*/
const setMarker = (pos, markerType) => {
const selElems = svgCanvas.getSelectedElements()
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.getSelectedElements()
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>'
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)
}
}
}
}
}
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="editor_extensions_ext-layer_view_locale_zh-CN.module_js.html">editor/extensions/ext-layer_view/locale/zh-CN.js</a></li><li><a href="module-SVGEditor.html">SVGEditor</a></li><li><a href="module-contextmenu.html">contextmenu</a></li><li><a href="module-jGraduate.html">jGraduate</a></li><li><a href="module-jPicker.html">jPicker</a></li><li><a href="module-locale.html">locale</a></li></ul><h3>Externals</h3><ul><li><a href="external-JamilihArray.html">JamilihArray</a></li><li><a href="external-Math.html">Math</a></li><li><a href="external-Window.html">Window</a></li><li><a href="external-jQuery.html">jQuery</a></li></ul><h3>Namespaces</h3><ul><li><a href="external-jQuery.fn.html">fn</a></li><li><a href="external-jQuery.fn.$.fn.jPicker.defaults.html">defaults</a></li><li><a href="external-jQuery.fn.exports.jPickerMethod.html">exports.jPickerMethod</a></li><li><a href="external-jQuery.fn.jGraduateDefaults.html">jGraduateDefaults</a></li><li><a href="external-jQuery.fn.jGraduateDefaults.images.html">images</a></li><li><a href="external-jQuery.fn.jGraduateDefaults.window.html">window</a></li><li><a href="external-jQuery.jGraduate.html">jGraduate</a></li><li><a href="external-jQuery.jPicker.html">jPicker</a></li><li><a href="external-jQuery.jPicker.ColorMethods.html">ColorMethods</a></li></ul><h3>Classes</h3><ul><li><a href="BottomPanel.html">BottomPanel</a></li><li><a href="Dropdown.html">Dropdown</a></li><li><a href="EditorStartup.html">EditorStartup</a></li><li><a href="ElixMenuButton.html">ElixMenuButton</a></li><li><a href="ElixNumberSpinBox.html">ElixNumberSpinBox</a></li><li><a href="ExplorerButton.html">ExplorerButton</a></li><li><a href="FlyingButton.html">FlyingButton</a></li><li><a href="LayersPanel.html">LayersPanel</a></li><li><a href="LeftPanel.html">LeftPanel</a></li><li><a href="MainMenu.html">MainMenu</a></li><li><a href="NumberSpinBox.html">NumberSpinBox</a></li><li><a href="PaintBox.html">PaintBox</a></li><li><a href="PlainNumberSpinBox.html">PlainNumberSpinBox</a></li><li><a href="Rulers.html">Rulers</a></li><li><a href="SEInput.html">SEInput</a></li><li><a href="SEPalette.html">SEPalette</a></li><li><a href="SESpinInput.html">SESpinInput</a></li><li><a href="SeCMenuDialog.html">SeCMenuDialog</a></li><li><a href="SeCMenuLayerDialog.html">SeCMenuLayerDialog</a></li><li><a href="SeColorPicker.html">SeColorPicker</a></li><li><a href="SeEditPrefsDialog.html">SeEditPrefsDialog</a></li><li><a href="SeExportDialog.html">SeExportDialog</a></li><li><a href="SeImgPropDialog.html">SeImgPropDialog</a></li><li><a href="SeList.html">SeList</a></li><li><a href="SeMenu.html">SeMenu</a></li><li><a href="SeMenuItem.html">SeMenuItem</a></li><li><a href="SePlainAlertDialog.html">SePlainAlertDialog</a></li><li><a href="SePlainBorderButton.html">SePlainBorderButton</a></li><li><a href="SePromptDialog.html">SePromptDialog</a></li><li><a href="SeStorageDialog.html">SeStorageDialog</a></li><li><a href="SeSvgSourceEditorDialog.html">SeSvgSourceEditorDialog</a></li><li><a href="SeText.html">SeText</a></li><li><a href="ToolButton.html">ToolButton</a></li><li><a href="TopPanel.html">TopPanel</a></li><li><a href="configObj.html">configObj</a></li><li><a href="external-jQuery.jGraduate.Paint.html">Paint</a></li><li><a href="external-jQuery.jPicker.Color.html">Color</a></li><li><a href="module.exports.html">exports</a></li><li><a href="module.exports_module.exports.html">exports</a></li><li><a href="module-SVGEditor-Editor.html">Editor</a></li><li><a href="module-jPicker.module.exports.html">module.exports</a></li></ul><h3>Interfaces</h3><ul><li><a href="module-SVGEditor.Config.html">Config</a></li><li><a href="module-SVGEditor.Prefs.html">Prefs</a></li><li><a href="module-SVGthis.CustomHandler.html">CustomHandler</a></li><li><a href="module-locale.LocaleEditorInit.html">LocaleEditorInit</a></li></ul><h3>Events</h3><ul><li><a href="module-SVGEditor.html#event:event:svgEditorReadyEvent">svgEditorReadyEvent</a></li></ul><h3>Tutorials</h3><ul><li><a href="tutorial-CanvasAPI.html">CanvasAPI</a></li><li><a href="tutorial-Editor.html">Editor</a></li><li><a href="tutorial-EditorAPI.html">EditorAPI</a></li><li><a href="tutorial-Events.html">Events</a></li><li><a href="tutorial-FrequentlyAskedQuestions.html">Frequently Asked Questions (FAQ)</a></li></ul><h3>Global</h3><ul><li><a href="global.html#attributeChangedCallback">attributeChangedCallback</a></li><li><a href="global.html#connectedCallback">connectedCallback</a></li><li><a href="global.html#constructor">constructor</a></li><li><a href="global.html#createTemplate">createTemplate</a></li><li><a href="global.html#decrement">decrement</a></li><li><a href="global.html#expireCookie">expireCookie</a></li><li><a href="global.html#formatValueFormatthenumericvalueasastring.Thisisusedafterincrementing/decrementingthevaluetoreformatthevalueasastring.">formatValue
Format the numeric value as a string.
This is used after incrementing/decrementing the value to reformat the
value as a string.</a></li><li><a href="global.html#get">get</a></li><li><a href="global.html#handleClick">handleClick</a></li><li><a href="global.html#handleClose">handleClose</a></li><li><a href="global.html#handleInput">handleInput</a></li><li><a href="global.html#handleKeyDown">handleKeyDown</a></li><li><a href="global.html#handleMouseDown">handleMouseDown</a></li><li><a href="global.html#handleMouseUp">handleMouseUp</a></li><li><a href="global.html#handleOptionsChange">handleOptionsChange</a></li><li><a href="global.html#handleSelect">handleSelect</a></li><li><a href="global.html#handleShow">handleShow</a></li><li><a href="global.html#increment">increment</a></li><li><a href="global.html#init">init</a></li><li><a href="global.html#inputsize">inputsize</a></li><li><a href="global.html#isNullish">isNullish</a></li><li><a href="global.html#loadloadConfig">load load Config</a></li><li><a href="global.html#loadFromURLLoadconfig/datafromURLifgiven">loadFromURL Load config/data from URL if given</a></li><li><a href="global.html#name">name</a></li><li><a href="global.html#observedAttributes">observedAttributes</a></li><li><a href="global.html#parseValue">parseValue</a></li><li><a href="global.html#pref">pref</a></li><li><a href="global.html#readySignal">readySignal</a></li><li><a href="global.html#regexEscape">regexEscape</a></li><li><a href="global.html#removeStoragePrefCookie">removeStoragePrefCookie</a></li><li><a href="global.html#replaceStoragePrompt">replaceStoragePrompt</a></li><li><a href="global.html#set">set</a></li><li><a href="global.html#setupCurConfig">setupCurConfig</a></li><li><a href="global.html#setupCurPrefs">setupCurPrefs</a></li><li><a href="global.html#src">src</a></li><li><a href="global.html#stateEffects">stateEffects</a></li><li><a href="global.html#stepDown">stepDown</a></li><li><a href="global.html#stepUp">stepUp</a></li><li><a href="global.html#triggerInputChanged">triggerInputChanged</a></li><li><a href="global.html#updateLib">updateLib</a></li><li><a href="global.html#value">value</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.5</a> on Sun Dec 07 2025 19:46:40 GMT+0100 (Central European Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>