如何在页面上的网页元素之间绘制连接线

4
我希望找到最简单的方法来绘制组件之间的简单线条,即使可能没有使用任何库(这是一个学习练习)。元素是代表卡片的div,始终垂直堆叠可能永远。卡片可以有不同的高度。该线将从给定元素的左侧退出(卡a),向上转90度,再次转90度进入另一个元素(卡b)。

Example

我尝试了一些方法。到目前为止,我还没有找到完全可行的方法,它们似乎都需要花费大量时间来弄清楚。我想知道的是什么是正确/首选的方法,以便我在正确的事情上花费时间,并且具有以下视图的未来证明:

  1. 我可以在任何两个框之间添加所需数量的连接线,而不仅仅是连续的框之间。
  2. 这些线会遵循大小调整并向下和向上滚动卡片。
  3. 某些卡可能没有终点,而是在页面的左上角终止,等待其卡滚动到视图中或创建。

尝试

我的第一个想法是使用左侧的全列组件中的 <canvas>,但将画布及其中的图形与我的 div 对齐是很麻烦的,还有无限滚动的画布。没能让它工作。

接下来我尝试了 <div>。就像 McBrackets这里所做的一样。给

的顶部、底部和外边缘着色,并将其与两个相关的卡片对齐,但是虽然我可以相对于卡片a定位它,但我无法弄清如何停止在卡片b处。

最后我尝试了<SVG>。只需使用.getElementById(),然后添加一个遵循上述指令的SVG路径。

    const connectingPath =
        "M " + aRect.left + " " + aRect.top + " " +
        "H " + (aRect.left - 50) +
        "V " + (bRect.top) +
        "H " + (bRect.left);

似乎没有什么东西是对齐的,调试起来相当困难,看起来需要考虑更复杂的解决方案,因为我需要考虑调整大小等因素。


@Moritur那对于我想连接A-C的情况,这个可以工作吗? - Ross Drew
1
SVG在这里是正确的解决方案...现在只需要把所有东西排列好即可。 - Brad
@Brad,一开始感觉这是正确的解决方案。非常简单,直到一切开始变得有些不稳定,我开始怀疑自己是否又走上了另一条疯狂的道路。因此提出这个问题。 - Ross Drew
1
最简单的方法是使用一个 viewBox="0 0 100 100" 和 preserveAspectRatio ="none" 的 svg 元素。svg 元素会被拉伸到容器的大小,并使用项目位置(百分比)的值来绘制连接器。为了避免变形,请在路径中使用 vector-effect: non-scaling-stroke;。请查看此示例:https://codepen.io/giaco/pen/xeWLvM - enxaneta
1
关于我提供的答案,还有一个最后的观察。如果这些卡片正在动态地进出DOM,那么在scroll事件中将会有更多的工作要做,以检查所有连接器是否可见。如果任何一部分可见,则使用addConnector进行渲染。 - GenericUser
显示剩余4条评论
1个回答

4

通过从要连接的盒子中获取一些测量值,例如offsetTopclientHeight,你可能可以应用这样的东西。


更新:添加了一些逻辑以满足未绘制卡片的要求。

虽然这并不能完全模拟卡片动态填充的情况,但我进行了更新以展示如何处理仅绘制一个卡片的情况。

  1. 使用默认值(1和5)单击连接。这将显示从框1开始的开放式连接器。
  2. 单击“添加框5”。这将添加缺少的框并更新连接器。

剩下的工作是在 scroll 上创建事件监听器以检查连接器列表。从那里,你可以检查 DOM 中是否都出现了两个盒子(参见 checkConnectors 函数)。如果它们都出现了,则传递值给 addConnector,它将完全连接它们。

class Container {
  constructor(element) {
    this.connectors = new Map();
    this.element = element;
  }

  addConnector(topBox, bottomBox, displayHalf = false) {
    if (!topBox && !bottomBox) throw new Error("Invalid params");
    const connector = new Connector(topBox, bottomBox, displayHalf);
    const connectorId = `${topBox.id}:${bottomBox.id}`;
    this.element.appendChild(connector.element);
    if (this.connectors.has(connectorId)) {
      connector.element.style.borderColor = this.connectors.get(connectorId).element.style.borderColor;
    } else {
      connector.element.style.borderColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
    }
    this.connectors.set(connectorId, connector);
  }

  checkConnectors() {
    this.connectors.forEach((connector) => {
      if (connector.displayHalf) {
        connector.firstBox.updateElement();
        connector.secondBox.updateElement();

        if (connector.firstBox.element && connector.secondBox.element) {
          this.addConnector(connector.firstBox, connector.secondBox);
        }
      }
    });
  }
}

class Box {
  constructor(id) {
    this.id = id;
    this.updateElement();
  }

  getMidpoint() {
    return this.element.offsetTop + this.element.clientHeight / 2;
  }

  updateElement() {
    this.element ??= document.getElementById(`box${this.id}`);
  }

  static sortTopDown(firstBox, secondBox) {
    return [firstBox, secondBox].sort((a,b) => a.element.offsetTop - b.element.offsetTop);
  }
}

class Connector {
  constructor(firstBox, secondBox, displayHalf) {
    this.firstBox = firstBox;
    this.secondBox = secondBox;
    this.displayHalf = displayHalf;
    const firstBoxHeight = this.firstBox.getMidpoint();
    this.element = document.createElement("div");
    this.element.classList.add("connector");
    this.element.style.top = firstBoxHeight + "px";
    let secondBoxHeight;
    if (this.displayHalf) {
      secondBoxHeight = this.firstBox.element.parentElement.clientHeight;
      this.element.style.borderBottom = "unset";
    } else {
      secondBoxHeight = this.secondBox.getMidpoint();
    }
    this.element.style.height = Math.abs(secondBoxHeight - firstBoxHeight) + "px";
  }
}

const connectButton = document.getElementById("connect");
const error = document.getElementById("error");
const addBoxButton = document.getElementById("addBox");
const container = new Container(document.getElementById("container"));

connectButton.addEventListener("click", () => {
  const firstBoxId = document.getElementById("selectFirstBox").value;
  const secondBoxId = document.getElementById("selectSecondBox").value;
  if (firstBoxId === "" || secondBoxId === "") return;
  error.style.display = firstBoxId === secondBoxId ? "block" : "none";
  const firstBox = new Box(firstBoxId);
  const secondBox = new Box(secondBoxId);
  // Check for undrawn cards  
  if (!!firstBox.element ^ !!secondBox.element) {
    return container.addConnector(firstBox, secondBox, true);
  }
  const [topBox, bottomBox] = Box.sortTopDown(firstBox, secondBox);  
  container.addConnector(topBox, bottomBox);
});

window.addEventListener("resize", () => container.checkConnectors());

addBoxButton.addEventListener("click", () => {
  const box = document.createElement("div");
  box.innerText = 5;
  box.id = "box5";
  box.classList.add("box");
  container.element.appendChild(box);
  addBoxButton.style.display = 'none';  
  container.checkConnectors();
});
.box {
  border: solid 1px;
  width: 60px;
  margin-left: 30px;
  margin-bottom: 5px;
  text-align: center;
}

#inputs {
  margin-top: 20px;
}

#inputs input {
  width: 150px;
}

.connector {
  position: absolute;
  border-top: solid 1px;
  border-left: solid 1px;
  border-bottom: solid 1px;
  width: 29px;
}

#error {
  display: none;
  color: red;
}
<div id="container">
  <div id="box1" class="box">1</div>
  <div id="box2" class="box">2</div>
  <div id="box3" class="box">3</div>
  <div id="box4" class="box">4</div>
</div>
<div id="inputs">
  <input id="selectFirstBox" type="number" placeholder="Provide first box id" min="1" value="1" max="5" />
  <input id="selectSecondBox" type="number" placeholder="Provide second box id" min="1" value="5" max="5" />
  <div id="error">Please select different boxes to connect.</div>
</div>
<button id="connect">Connect</button>
<button id="addBox">Add box 5</button>


1
运行得非常好。实际上,我只是想知道哪种方法最好,你编写了一个可以操作的完整解决方案,非常感谢。 - Ross Drew
1
@RossDrew 它随着时间的推移有点增长,哈哈。无论如何,很高兴能够帮助。 - GenericUser

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接