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";
|
import { default as HorizontalScroller } from "./../back/routes/common/horizontal-scroller/horizontal-scroller.stimulus.js";
|
||||||
application.register("horizontal-scroller", HorizontalScroller);
|
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";
|
import { default as AutogrowTextarea } from "./../back/routes/component-preview/autogrow-textarea.stimulus.js";
|
||||||
application.register("autogrow-textarea", AutogrowTextarea);
|
application.register("autogrow-textarea", AutogrowTextarea);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user