Sortable grid checkpoint

This commit is contained in:
Kuba Orlik 2024-08-30 19:13:21 +02:00
parent b8e0f3662a
commit dd4d4dfb3d
5 changed files with 220 additions and 0 deletions

View File

@ -0,0 +1,51 @@
.sortable {
display: grid;
grid-template-columns: 1fr;
--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 {
grid-column: 1/2;
opacity: 0.5;
z-index: 1;
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 100ms;
height: 8px;
}
}

View File

@ -0,0 +1,101 @@
import { Controller } from "stimulus";
export default class Sortable extends Controller {
dragged_element: HTMLDivElement | null = null;
clearDoubleHoles() {
this.element
.querySelectorAll(".ready-to-drop")
.forEach((e: HTMLDivElement) => e.classList.remove("ready-to-drop"));
}
getNthElement(n: number): HTMLDivElement | null {
return this.element.querySelector(`.sortable__element:nth-child(${n})`);
}
setIndex(node: HTMLDivElement, index: number) {
node.setAttribute("data-index", String(index));
(node as HTMLDivElement).style.setProperty("--index", String(index));
}
setupHoleListeners(hole: HTMLDivElement) {
hole.addEventListener("dragenter", (event) => {
if (!this.dragged_element) {
return;
}
(event.target as HTMLDivElement).classList.add("ready-to-drop");
event.preventDefault();
});
hole.addEventListener("dragover", (event) => {
if (!this.dragged_element) {
return;
}
event.preventDefault();
});
hole.addEventListener("dragleave", (event) => {
(event.target as HTMLDivElement).classList.remove("ready-to-drop");
event.preventDefault();
});
hole.addEventListener("drop", (event) => {
const target = event.target as HTMLDivElement;
target.classList.remove("ready-to-drop");
const index_of_dropped_element = parseInt(
this.dragged_element.getAttribute("data-index")
);
const index_of_drop_target = parseInt(target.getAttribute("data-index"));
const nodes_of_dropped_element_index = this.element.querySelectorAll(
`[data-index="${index_of_dropped_element}"]`
);
const nodes_of_target_index = this.element.querySelectorAll(
`[data-index="${index_of_drop_target}"]`
);
let last_node_of_target_index =
nodes_of_target_index[nodes_of_target_index.length - 1];
for (const node of Array.from(nodes_of_dropped_element_index)) {
last_node_of_target_index.after(node);
this.setIndex(node as HTMLDivElement, index_of_drop_target);
last_node_of_target_index = node;
}
let next_to_correct = nodes_of_dropped_element_index[0].previousSibling;
const children = Array.from(next_to_correct.parentNode.childNodes);
const children_to_correct = children.slice(2);
for (const dom_index in children_to_correct) {
const index = Math.max(Math.floor(parseInt(dom_index) / 3), 0);
this.setIndex(children_to_correct[dom_index] as HTMLDivElement, index);
}
event.preventDefault();
});
}
connect() {
this.element.querySelectorAll(".sortable__element").forEach((element) => {
element.addEventListener("dragstart", (e: DragEvent) => {
e.dataTransfer.dropEffect = "move";
const target = e.target as HTMLDivElement;
this.dragged_element = target;
setTimeout(() => {
// https://stackoverflow.com/a/20733870/1467284
target.classList.add("is-dragged");
}, 0);
});
element.addEventListener("dragend", (e: DragEvent) => {
const target = e.target as HTMLDivElement;
this.dragged_element = null;
target.classList.remove("is-dragged");
});
});
this.element
.querySelectorAll(".sortable__hole")
.forEach((dropElement: HTMLDivElement) =>
this.setupHoleListeners(dropElement)
);
}
}

View File

@ -0,0 +1,41 @@
import { TempstreamJSX } from "tempstream";
export function sortable({ items }: { items: JSX.Element[] }) {
return (
<div>
<h2>Sortable</h2>
<div
data-controller="sortable"
class="sortable"
style={`grid-template-rows: repeat(${
items.length * 2
}, minmax(8px, min-content))`}
>
<div class="sortable__hole" data-index="-1" style="--index: -1"></div>
<div class="sortable__spacer" data-index="-1" style="--index: -1"></div>
{items.map((item, index) => (
<>
<div
class="sortable__element"
draggable="true"
style={`--index: ${index}`}
data-index={index.toString()}
>
{item}
</div>
<div
class="sortable__hole"
style={`--index: ${index}`}
data-index={index.toString()}
></div>
<div
class="sortable__spacer"
style={`--index: ${index}`}
data-index={index.toString()}
></div>
</>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,24 @@
import type { Context } from "koa";
import { TempstreamJSX } from "tempstream";
import { Page } from "@sealcode/sealgen";
import html from "../../html.js";
import { sortable } from "../common/sortable/sortable.js";
export const actionName = "SortableDemo";
export default new (class SortableDemoPage extends Page {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async canAccess(_: Context) {
return { canAccess: true, message: "" };
}
async render(ctx: Context) {
return html(
ctx,
"SortableDemo",
sortable({
items: ["One", "Two", "Three", "Four", "Five"].map((e) => <div>{e}</div>),
})
);
}
})();

View File

@ -19,6 +19,9 @@ application.register("map-with-pins", MapWithPins);
import { default as HorizontalScroller } from "./../back/routes/common/horizontal-scroller/horizontal-scroller.stimulus.js";
application.register("horizontal-scroller", HorizontalScroller);
import { default as Sortable } from "./../back/routes/common/sortable/sortable.stimulus.js";
application.register("sortable", Sortable);
import { default as AutogrowTextarea } from "./../back/routes/component-preview/autogrow-textarea.stimulus.js";
application.register("autogrow-textarea", AutogrowTextarea);