reroute
This commit is contained in:
114
src/components/category/CategorySortable.js
Normal file
114
src/components/category/CategorySortable.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import Sortable from 'sortablejs';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
/**
|
||||
* Encapsulates SortableJS logic for reordering category tiles inside a grid.
|
||||
*
|
||||
* Usage:
|
||||
* this.sortable = new CategorySortable(containerEl, { onReorder });
|
||||
* this.sortable.activate();
|
||||
* this.sortable.destroy();
|
||||
* this.sortable.sortableIds(); // current DOM order
|
||||
* this.sortable.handleMoveCategory(id, direction); // +1 / -1
|
||||
*/
|
||||
export default class CategorySortable {
|
||||
#instance = null;
|
||||
#container = null;
|
||||
#onReorder = null;
|
||||
#checkpointOrder = null;
|
||||
|
||||
constructor(container, { onReorder }) {
|
||||
this.#container = container;
|
||||
this.#onReorder = onReorder;
|
||||
}
|
||||
|
||||
activate() {
|
||||
const el = this.#container;
|
||||
if (!el || this.#instance) return;
|
||||
|
||||
this.#instance = Sortable.create(el, {
|
||||
draggable: '.sortable-item:not(.not-sortable)',
|
||||
handle: '.sortable-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.not-sortable, .sortable-handle-btn',
|
||||
forceFallback: true,
|
||||
fallbackOnBody: true,
|
||||
onStart: this.#handleSortStart,
|
||||
onEnd: this.#handleSort,
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.#instance) {
|
||||
this.#instance.destroy();
|
||||
this.#instance = null;
|
||||
}
|
||||
this.#checkpointOrder = null;
|
||||
document.body.classList.remove('tree-dragging');
|
||||
}
|
||||
|
||||
sortableIds() {
|
||||
const container = this.#container;
|
||||
if (!container) return [];
|
||||
return [...container.querySelectorAll('.sortable-item:not(.not-sortable)')]
|
||||
.map((el) => el.dataset.id)
|
||||
.filter(Boolean)
|
||||
.map(Number);
|
||||
}
|
||||
|
||||
handleMoveCategory = (categoryId, direction) => {
|
||||
const ids = this.sortableIds();
|
||||
const index = ids.indexOf(categoryId);
|
||||
if (index === -1) return;
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= ids.length) return;
|
||||
const next = [...ids];
|
||||
next[index] = ids[target];
|
||||
next[target] = categoryId;
|
||||
void this.#onReorder({ categoryIds: next }).unwrap().catch(notifyError);
|
||||
};
|
||||
|
||||
revertDomOrder = (order) => {
|
||||
const container = this.#container;
|
||||
if (!container) return;
|
||||
const items = [...container.querySelectorAll('.sortable-item')];
|
||||
const byId = new Map(items.map((el) => [Number(el.dataset.id), el]));
|
||||
const anchor =
|
||||
[...container.children].find((el) => !el.classList.contains('sortable-item')) ?? null;
|
||||
for (const id of order) {
|
||||
const el = byId.get(id);
|
||||
if (!el) continue;
|
||||
if (anchor) container.insertBefore(el, anchor);
|
||||
else container.appendChild(el);
|
||||
}
|
||||
};
|
||||
|
||||
// --- private ---
|
||||
|
||||
#handleSortStart = () => {
|
||||
this.#checkpointOrder = this.sortableIds();
|
||||
document.body.classList.add('tree-dragging');
|
||||
};
|
||||
|
||||
#handleSort = () => {
|
||||
document.body.classList.remove('tree-dragging');
|
||||
const ids = this.sortableIds();
|
||||
const before = this.#checkpointOrder ?? [];
|
||||
this.#checkpointOrder = null;
|
||||
if (
|
||||
before.length === 0 ||
|
||||
before.length !== ids.length ||
|
||||
before.every((id, index) => id === ids[index])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void this.#onReorder({ categoryIds: ids })
|
||||
.unwrap()
|
||||
.catch((err) => {
|
||||
notifyError(err);
|
||||
this.revertDomOrder(before);
|
||||
});
|
||||
};
|
||||
}
|
||||
42
src/components/category/categoryRoute.js
Normal file
42
src/components/category/categoryRoute.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
ROOT_CATEGORY_NAME,
|
||||
categorySlug,
|
||||
getCategoryBreadcrumbs,
|
||||
getCategoryChildren,
|
||||
} from '../../utils/categoryUtils';
|
||||
|
||||
export { ROOT_CATEGORY_NAME, categorySlug };
|
||||
|
||||
export const KATEGORIEN_BASE = '/kategorien';
|
||||
|
||||
export function isKategorienPath(pathname) {
|
||||
return pathname === KATEGORIEN_BASE || pathname.startsWith(`${KATEGORIEN_BASE}/`);
|
||||
}
|
||||
|
||||
function isRootSlug(slug) {
|
||||
return slug === ROOT_CATEGORY_NAME;
|
||||
}
|
||||
|
||||
export function categoryPathFromCrumbs(crumbs) {
|
||||
const segments = (crumbs ?? [])
|
||||
.filter((crumb) => crumb.slug && !isRootSlug(crumb.slug))
|
||||
.map((crumb) => encodeURIComponent(crumb.slug));
|
||||
if (segments.length === 0) {
|
||||
return KATEGORIEN_BASE;
|
||||
}
|
||||
return `${KATEGORIEN_BASE}/${segments.join('/')}`;
|
||||
}
|
||||
|
||||
export function buildCategoryPath(categories, rootCategoryId, categoryId) {
|
||||
const crumbs = getCategoryBreadcrumbs(categories, categoryId, rootCategoryId);
|
||||
return categoryPathFromCrumbs(crumbs);
|
||||
}
|
||||
|
||||
export {
|
||||
resolveCategoryIdFromSlugPath,
|
||||
cleanCategorySlugPath,
|
||||
} from '../../utils/categoryUtils';
|
||||
|
||||
export function defaultCategoryPath() {
|
||||
return KATEGORIEN_BASE;
|
||||
}
|
||||
19
src/components/category/withCategoryRoute.jsx
Normal file
19
src/components/category/withCategoryRoute.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
export default function withCategoryRoute(Wrapped) {
|
||||
function CategoryRouteWrapper(props) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { '*': categorySlugPath = '' } = useParams();
|
||||
|
||||
return (
|
||||
<Wrapped
|
||||
{...props}
|
||||
navigate={navigate}
|
||||
location={location}
|
||||
categorySlugPath={categorySlugPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return CategoryRouteWrapper;
|
||||
}
|
||||
Reference in New Issue
Block a user