Easier scrolling while dragging in sortable

This commit is contained in:
Kuba Orlik 2024-08-31 16:36:30 +02:00
parent ecd3f45081
commit 8f86a99116
4 changed files with 122 additions and 46 deletions

View File

@ -1,51 +1,80 @@
.sortable { @keyframes sortable-enter {
display: grid; from {
grid-template-columns: 1fr; transform: scale(0);
--element-height: 50px;
--gap: 8px;
.sortable__element {
height: var(--element-height);
background-color: white;
box-sizing: border-box;
min-width: 400px;
font-size: 20px;
border: 1px solid black;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
user-select: none;
grid-column: 1/2;
grid-row: calc((var(--index) + 1) * 3 + 1) / calc((var(--index) + 1) * 3 + 3);
&.is-dragged {
opacity: 0.2;
}
} }
.sortable__hole { to {
grid-column: 1/2; transform: scale(1);
opacity: 0.5; }
z-index: 1; }
pointer-events: none; .sortable-wrapper {
grid-row: calc((var(--index) + 1) * 3 + 2) / calc((var(--index) + 1) * 3 + 5); .edge-detector {
height: 30px;
&.ready-to-drop + .sortable__spacer { position: fixed;
height: var(--element-height) !important; left: 0;
} width: 100%;
display: none;
z-index: 2;
} }
&:has(.is-dragged) { &:has(.is-dragged) {
.sortable__hole { .edge-detector {
pointer-events: all; display: block;
} }
} }
.sortable__spacer { .sortable {
grid-column: 1/2; display: grid;
grid-row: calc((var(--index) + 1) * 3 + 3) / calc((var(--index) + 1) * 3 + 4); grid-template-columns: 1fr;
transition: all 100ms; --element-height: 50px;
height: 8px; --gap: 8px;
.sortable__element {
height: var(--element-height);
grid-column: 1/2;
grid-row: calc((var(--index) + 1) * 3 + 1) / calc((var(--index) + 1) * 3 + 3);
background-color: white;
box-sizing: border-box;
min-width: 400px;
font-size: 20px;
border: 1px solid black;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
user-select: none;
animation: sortable-enter 100ms;
&.is-dragged {
opacity: 0.2;
}
}
.sortable__hole {
grid-column: 1/2;
z-index: 1;
/* background-color: red; */
pointer-events: none;
grid-row: calc((var(--index) + 1) * 3 + 2) / calc((var(--index) + 1) * 3 + 5);
&.ready-to-drop + .sortable__spacer {
height: var(--element-height) !important;
}
}
&:has(.is-dragged) {
.sortable__hole {
pointer-events: all;
}
}
.sortable__spacer {
grid-column: 1/2;
grid-row: calc((var(--index) + 1) * 3 + 3) / calc((var(--index) + 1) * 3 + 4);
transition: all 200ms;
height: 8px;
}
} }
} }

View File

@ -1,5 +1,11 @@
import { Controller } from "stimulus"; import { Controller } from "stimulus";
async function sleep(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
}
export default class Sortable extends Controller { export default class Sortable extends Controller {
dragged_element: HTMLDivElement | null = null; dragged_element: HTMLDivElement | null = null;
@ -79,7 +85,7 @@ export default class Sortable extends Controller {
connect() { connect() {
this.element.querySelectorAll(".sortable__element").forEach((element) => { this.element.querySelectorAll(".sortable__element").forEach((element) => {
element.addEventListener("dragstart", (e: DragEvent) => { element.addEventListener("dragstart", (e: DragEvent) => {
event.dataTransfer.effectAllowed = "move"; e.dataTransfer.effectAllowed = "move";
const target = e.target as HTMLDivElement; const target = e.target as HTMLDivElement;
this.dragged_element = target; this.dragged_element = target;
setTimeout(() => { setTimeout(() => {
@ -94,6 +100,32 @@ export default class Sortable extends Controller {
target.classList.remove("is-dragged"); target.classList.remove("is-dragged");
}); });
}); });
this.element
.querySelectorAll(".edge-detector")
.forEach((detector: HTMLDivElement) => {
let is_hovered = false;
detector.addEventListener("dragenter", async (e) => {
e.preventDefault();
const target = e.target as HTMLDivElement;
const step = parseInt(target.getAttribute("data-step"));
is_hovered = true;
while (is_hovered && this.dragged_element) {
window.scrollTo(window.scrollX, window.scrollY + step);
await sleep(16);
}
});
detector.addEventListener("dragover ", (e) => {
// necessary for drag events;
e.preventDefault();
});
detector.addEventListener("dragleave", (e) => {
const target = e.target as HTMLDivElement;
e.preventDefault();
is_hovered = false;
});
});
this.element this.element
.querySelectorAll(".sortable__hole") .querySelectorAll(".sortable__hole")
.forEach((dropElement: HTMLDivElement) => .forEach((dropElement: HTMLDivElement) =>

View File

@ -2,7 +2,7 @@ import { TempstreamJSX } from "tempstream";
export function sortable({ items }: { items: JSX.Element[] }) { export function sortable({ items }: { items: JSX.Element[] }) {
return ( return (
<div> <div class="sortable-wrapper">
<div <div
data-controller="sortable" data-controller="sortable"
class="sortable" class="sortable"
@ -10,6 +10,8 @@ export function sortable({ items }: { items: JSX.Element[] }) {
items.length * 2 items.length * 2
}, minmax(8px, min-content))`} }, minmax(8px, min-content))`}
> >
<div class="edge-detector" style="top: 0" data-step="-10"></div>
<div class="edge-detector" style="bottom: 0" data-step="10"></div>
<div class="sortable__hole" data-index="-1" style="--index: -1"></div> <div class="sortable__hole" data-index="-1" style="--index: -1"></div>
<div class="sortable__spacer" data-index="-1" style="--index: -1"></div> <div class="sortable__spacer" data-index="-1" style="--index: -1"></div>
{items.map((item, index) => ( {items.map((item, index) => (

View File

@ -16,9 +16,22 @@ export default new (class SortableDemoPage extends Page {
return html( return html(
ctx, ctx,
"SortableDemo", "SortableDemo",
sortable({ <div>
items: ["One", "Two", "Three", "Four", "Five"].map((e) => <div>{e}</div>), <h2>Short list</h2>
}) {sortable({
items: ["One", "Two", "Three", "Four", "Five"].map((e) => (
<div>{e}</div>
)),
})}
<h2>Long list</h2>
{sortable({
items: "a"
.repeat(100)
.split("")
.map((_, index) => <div>{index}</div>),
})}
</div>
); );
} }
})(); })();