Sortable grid checkpoint
This commit is contained in:
parent
b8e0f3662a
commit
dd4d4dfb3d
51
src/back/routes/common/sortable/sortable.css
Normal file
51
src/back/routes/common/sortable/sortable.css
Normal 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;
|
||||
}
|
||||
}
|
101
src/back/routes/common/sortable/sortable.stimulus.ts
Normal file
101
src/back/routes/common/sortable/sortable.stimulus.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
41
src/back/routes/common/sortable/sortable.tsx
Normal file
41
src/back/routes/common/sortable/sortable.tsx
Normal 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>
|
||||
);
|
||||
}
|
24
src/back/routes/demos/sortable.page.tsx
Normal file
24
src/back/routes/demos/sortable.page.tsx
Normal 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>),
|
||||
})
|
||||
);
|
||||
}
|
||||
})();
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user