I want to have a panel that appears when clicking on a child element of an SVG.
This panel should display the properties
of the selected object,
and allow direct modification of those properties, which would then affect the element. How can I achieve this?
(pure javascript no use any package)
svgElem.onclick
)div
)input
field in the panelinput.onchange
event handler to update the element's attribute: input.onchange = () => {element.setAttribute(attr.name, input.value)}
<style>
.selected {outline: 2px dashed red;}
body {display: flex;}
</style>
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<rect width="30" height="60" x="80" y="10" style="transform: rotate(30deg)"/>
<line x1="143" y1="100" x2="358" y2="55" fill="#000000" stroke="#000000" stroke-width="4" stroke-dasharray="8,8"
opacity="1"></line>
<circle cx="50" cy="50" r="10" fill="green"/>
</svg>
<div id="info-panel"></div>
<script>
const svgElement = document.querySelector('svg')
svgElement.addEventListener('click', (event) => {
document.querySelector(`[class~='selected']`)?.classList.remove("selected")
const targetElement = event.target
targetElement.classList.add("selected")
displayAttrsPanel(targetElement)
})
function displayAttrsPanel(element) {
const infoPanel = document.getElementById('info-panel')
infoPanel.innerHTML = ''
const attributes = element.attributes
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i]
const frag = createInputFragment(attr.name, attr.value)
const input = frag.querySelector('input')
input.onchange = () => {
element.setAttribute(attr.name, input.value)
}
infoPanel.append(frag)
}
}
function createInputFragment(name, value) {
return document.createRange()
.createContextualFragment(
`<div><label>${name}<input value="${value}"></div>`
)
}
</script>
The above example is a relatively concise version. The following example provides more settings, such as:
input.type={color, number, text}
, etc.<style>
.selected {outline: 2px dashed red;}
body {display: flex;}
</style>
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<rect width="30" height="60" x="80" y="10" style="transform: rotate(30deg);opacity: 0.8;"/>
<line x1="143" y1="100" x2="358" y2="55" fill="#000000" stroke="#000000" stroke-width="4" stroke-dasharray="8,8"
opacity="1"></line>
<circle class="cute big" cx="50" cy="50" r="10" fill="green"/>
<polygon points="225,124 114,195 168,288 293,251.123456"></polygon>
<polyline points="50,150 100,75 150,50 200,140 250,140" fill="yellow"></polyline>
<path d="M150 300 L75 200 L225 200 Z" fill="purple"></path>
</svg>
<div id="info-panel"></div>
<script>
const svgElement = document.querySelector('svg')
svgElement.addEventListener('click', (event) => {
document.querySelector(`[class~='selected']`)?.classList.remove("selected")
const targetElement = event.target
targetElement.classList.add("selected")
displayAttrsPanel(targetElement)
})
function displayAttrsPanel(element) {
const infoPanel = document.getElementById('info-panel')
infoPanel.innerHTML = ''
// Sorting is an optional feature designed to ensure the presentation order remains as fixed as possible.
const attributes = [...element.attributes].sort((a, b)=>{
return a.name < b.name ? -1 :
a.name > b.name ? 1 : 0
})
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i]
const frag = createInputFragment(attr.name, attr.value)
// add event
const input = frag.querySelector('input')
const deleteBtn = frag.querySelector('button')
input.onchange = () => {
element.setAttribute(attr.name, input.value)
}
// allow delete attribute
deleteBtn.onclick = () => {
element.removeAttribute(attr.name)
displayAttrsPanel(element) // refresh
}
// For special case, when clicking the label, sub-items can be displayed separately, making it convenient for editing.
const label = frag.querySelector("label")
if (["class", "style", "points", "d"].includes(attr.name)) {
label.style.backgroundColor = "#d8f9d8" // for the user know it can click
label.style.cursor = "pointer"
let splitFunc
const cbOptions = []
switch (attr.name) {
case "points": // https://www.w3schools.com/graphics/svg_polygon.asp
case "class":
splitFunc=value=>value.split(" ")
cbOptions.push((newValues)=>element.setAttribute(attr.name, newValues.join(" ")))
break
case "style":
splitFunc=value=>value.split(";")
cbOptions.push((newValues)=>element.setAttribute(attr.name, newValues.join(";")))
break
case "d": // https://www.w3schools.com/graphics/svg_path.asp
const regex = /([MLHVCSQTAZ])([^MLHVCSQTAZ]*)/g;
splitFunc=value=>value.match(regex)
cbOptions.push((newValues)=>element.setAttribute(attr.name, newValues.join("")))
}
label.addEventListener("click", () => {
openEditDialog(attr.name, attr.value,
splitFunc,
(newValues) => {
for (const option of cbOptions) {
option(newValues)
}
displayAttrsPanel(element) // refresh
})
})
}
infoPanel.append(frag)
}
// Add New Attribute Button
const frag = document.createRange()
.createContextualFragment(
`<div><label>+<input placeholder="attribute"><input placeholder="value"></label><button>Add</button></div>`
)
const [inputAttr, inputVal] = frag.querySelectorAll("input")
frag.querySelector("button").onclick = () => {
const name = inputAttr.value.trim()
const value = inputVal.value.trim()
if (name && value) {
element.setAttribute(name, value)
inputAttr.value = ''
inputVal.value = ''
displayAttrsPanel(element) // refresh
}
}
infoPanel.appendChild(frag)
}
function createInputFragment(name, value) {
const frag = document.createRange()
.createContextualFragment(
`<div><label>${name}</label><input value="${value}"><button>-</button></div>`
)
const input = frag.querySelector("input")
switch (name) {
case "stroke":
case "fill":
input.type = "color"
break
case "opacity":
input.type = "range"
input.step = "0.05"
input.max = "1"
input.min = "0"
break
case "cx":
case "cy":
case "r":
case "rx":
case "ry":
case "x":
case "y":
case "x1":
case "x2":
case "y1":
case "y2":
case "stroke-width":
input.type = "number"
break
default:
input.type = "text"
}
return frag
}
function openEditDialog(name, valueStr, splitFunc, callback) {
const frag = document.createRange()
.createContextualFragment(
`<dialog open>
<div style="display: flex;flex-direction: column;"></div>
<button id="add">Add</button>
<button id="save">Save</button></dialog>`
)
const dialog = frag.querySelector("dialog")
const divValueContainer = frag.querySelector('div')
const addBtn = frag.querySelector("button#add")
const saveBtn = frag.querySelector("button#save")
const values = splitFunc(valueStr)
for (const val of values) {
const input = document.createElement("input")
input.value = val
divValueContainer.append(input)
}
// Add
addBtn.onclick = () => {
const input = document.createElement("input")
divValueContainer.append(input)
}
// Save
saveBtn.onclick = () => {
const newValues = []
dialog.querySelectorAll("input").forEach(e=>{
if (e.value !== "") {
newValues.push(e.value)
}
})
callback(newValues)
dialog.close()
}
document.body.append(dialog)
}
</script>