JSFiddle - React, Tailwind, and code Playground
by frumbert
HTML
<link rel="stylesheet" href="https://unpkg.com/normalize.css">
<script src="https://fonts.googleapis.com/css?family=Nunito+Sans:200,300,400,600,700"></script>
<script src="https://fonts.googleapis.com/css?family=Montserrat:400,700"></script>
<script src="https://unpkg.com/leader-line"></script>
<script src="https://unpkg.com/plain-draggable"></script>
<script src="https://unpkg.com/anim-event"></script>
<main>
<section class="template split-10-90">
<div class="pane cover-image">
<img src="https://images.unsplash.com/photo-1509813685-e7f0e4eaf3ee?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=60">
</div>
<div class="pane overflow light">
<div class="padding">
<h1>Connect the items</h1>
<p>Drag connections between these items.</p>
<div class="split-connector">
<div>
<h3>Flavours</h3>
<div id="plugs" class="list-group boxes">
<div class="list-group-item"><span>Grape</span></div>
<div class="list-group-item"><span>Vanilla</span></div>
<div class="list-group-item"><span>Chocolate, but not that horrible stuff that Cadbury make</span></div>
<div class="list-group-item"><img src="https://images.unsplash.com/photo-1534308983496-4fabb1a015ee?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=60"></div>
<div class="list-group-item"><span>Lemon</span></div>
<div class="list-group-item"><span>Lime like you get in the packets of Starburst snakes</span></div>
</div>
</div>
<div class="connectors">
<div class="connector start"></div>
<div class="connector end"></div>
<div class="connector start"></div>
<div class="connector end"></div>
<div class="connector start"></div>
<div class="connector end"></div>
<div class="connector start"></div>
<div class="connector end"></div>
<div...
CSS
:root {
--text-fg: #222;
--text-size: 20px;
--button-bg: #31b7f9;
--button-bg-light: #a6dffc;
}
html {
box-sizing: border-box;
font-size: var(--text-size);
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Ubuntu, Roboto, Noto, "Segoe UI", Arial, sans-serif;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
* {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: transparent;
}
body,
html {
margin: 0;
padding: 0;
border: none;
height: 100%;
}
body {
letter-spacing: .01rem;
line-height: 1.8;
font-size: 1rem;
font-weight: 400;
font-family: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
letter-spacing: .01rem;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .004);
color: var(--text-fg);
overflow: hidden;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: Montserrat, sans-serif;
font-weight: 300;
line-height: 1.2;
margin: 0
}
h1 {
font-size: 3rem;
letter-spacing: .025rem
}
h2 {
font-size: 2.5rem
}
h3 {
font-size: 2rem
}
h4 {
font-size: 1.75rem
}
h5 {
font-size: 1.5rem
}
h6 {
font-size: 1.25rem
}
p {
font-size: 1rem
}
body,
main {
width: 100vw;
height: 100vh;
}
main {
position: relative;
}
.template {
background-color: white;
height: 100%;
}
[class*='stack-'],
[class*="split-"] {
display: flex;
transition: flex .3s;
}
[class*='stack-'] {
flex-direction: column;
}
/* note: treat flex values like its a percentage */
.stack-evenly>*,
.stack-50>*,
.split-evenly>*,
.split-50>* {
flex: 50;
}
.stack-30-70>*:first-of-type,
.split-30-70>*:first-of-type {
flex: 30;
}
.stack-30-70>*:last-of-type,
.split-30-70>*:last-of-type {
flex: 70;
}
.stack-20-80>*:first-of-type,
.split-20-80>*:first-of-type {
flex:...
JavaScript
const connectorsNode = document.querySelector(".connectors");
const scrollableNode = connectorsNode.closest(".overflow");
const connectors = connectorsNode.querySelectorAll(".connector.start");
const lineWrapper = document.getElementById("line-wrapper");
const plugs = document.getElementById("plugs");
const plugPosition = plugs.getBoundingClientRect().y - plugs.previousElementSibling.getBoundingClientRect().height; // looks wrong but we are looking for a negaitve offset
const sockets = document.getElementById("sockets");
const socketPosition = sockets.getBoundingClientRect().y - sockets.previousElementSibling.getBoundingClientRect().height; // looks wrong but we are looking for a negaitve offset
function remToPx(rem) {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
}
scrollableNode.addEventListener('scroll', AnimEvent.add(wrapperPosition), false);
function wrapperPosition() {
lineWrapper.style.transform = 'none';
const rect = lineWrapper.getBoundingClientRect();
const sx = scrollableNode.scrollLeft;// - pageXOffset;
const sy = scrollableNode.scrollTop; // - pageYOffset;
const x = pageXOffset;
const y = pageYOffset;
lineWrapper.style.transform = 'translate(' +
(-(rect.left + x + sx)) + 'px, ' +
(-(rect.top + y + sy)) + 'px)';
}
let ends = [], snaptargets = [];
Array.from(connectors).forEach(function(start, index) {
const end = start.nextElementSibling;
const plug = plugs.children[index];
const socket = sockets.children[index];
let position = plug.getBoundingClientRect();
let midpoint = (position.y - plugPosition) + (position.height / 2);
const calc = "calc(" + midpoint + "px - .5rem)";
start.style.top = calc;
end.style.top = calc;
position = socket.getBoundingClientRect();
midpoint = (socket.getBoundingClientRect().y - socketPosition) + (position.height / 2);
snaptargets.push({
x: connectorsNode.offsetWidth - remToPx(.75),
...