使用 UI 操作 SVG 元素和属性

问题描述 投票:0回答:1

我想要一个在单击 SVG 的子元素时出现的面板。

此面板应显示所选对象的

properties

并允许直接修改这些属性,这将影响元素。我怎样才能实现这个目标?

纯javascript不使用任何包

javascript svg
1个回答
0
投票
  1. 添加点击事件来检测哪个元素被点击(
    svgElem.onclick
    )
  2. 创建面板 (
    div
    )
  3. 对于每个属性(attributes可以获取当前所有属性),在面板中添加一个
    input
    字段
  4. 添加
    input.onchange
    事件处理程序来更新元素的属性:
    input.onchange = () => {element.setAttribute(attr.name, input.value)}

简单版

<style>
  .selected {stroke: 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>

完整代码

上面的例子是一个比较简洁的版本。以下示例提供了更多设置,例如:

  • type:输入可以进行简单的判断,区分是否是
    input.type={color, number, text}
  • 删除按钮:在每个属性上添加删除按钮。
  • 新属性按钮:通过面板添加新属性的能力
  • dialog:对于更复杂的属性,例如{class,style,d,points},可以使用单独的对话框来单独设置每个值

<style>
  .selected {stroke: 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 (option of cbOptions) {
                option(newValues)
              }
              displayAttrsPanel(element) // refresh
          })
        })
      }
      infoPanel.append(frag)
    }

    // Add New Attribute
    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>

© www.soinside.com 2019 - 2024. All rights reserved.