fix drag&drop to reorganize combos
All checks were successful
Deploy Sokko G / deploy (push) Successful in 8s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 8s
This commit is contained in:
parent
7b4cfb7dc2
commit
a6d35c6e6b
8 changed files with 724 additions and 282 deletions
|
|
@ -432,6 +432,8 @@ Type : `combos`
|
|||
```json
|
||||
{
|
||||
"device": "playstation",
|
||||
"categoryOrder": ["Neutral"],
|
||||
"collapsedCategories": ["Advanced"],
|
||||
"combos": [
|
||||
{
|
||||
"id": "combo0",
|
||||
|
|
@ -443,27 +445,16 @@ Type : `combos`
|
|||
{ "kind": "button", "value": "triangle" }
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "category1",
|
||||
"title": "Neutral",
|
||||
"collapsed": false,
|
||||
"combos": [
|
||||
},
|
||||
{
|
||||
"id": "combo1",
|
||||
"name": "Hadoken",
|
||||
"category": "Neutral",
|
||||
"device": "playstation",
|
||||
"inputs": [
|
||||
[{ "kind": "direction", "value": "down" }],
|
||||
[{ "kind": "direction", "value": "down-forward" }],
|
||||
[
|
||||
{ "kind": "direction", "value": "forward" },
|
||||
{ "kind": "button", "value": "cross" }
|
||||
]
|
||||
]
|
||||
}
|
||||
[{ "kind": "button", "value": "cross" }]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -474,20 +465,23 @@ Notes :
|
|||
|
||||
- `device` vaut `playstation`, `xbox`, `switch`, `n64` ou `keyboardMouse`.
|
||||
- `combos[].device` peut figer le périphérique de rendu d'un combo, afin qu'il conserve ses couleurs si l'outil change de périphérique.
|
||||
- `combos[].category` est facultatif ; les catégories sont déduites automatiquement de cette valeur.
|
||||
- `inputs` est une liste d'étapes successives.
|
||||
- chaque étape contient une ou plusieurs entrées simultanées.
|
||||
- `kind` vaut `button`, `direction`, `key` ou `mouse`.
|
||||
- `combos` à la racine contient les combos sans catégorie.
|
||||
- `categoryOrder` stocke l'ordre d'affichage des catégories utilisées.
|
||||
- `collapsedCategories` stocke les noms des catégories repliées.
|
||||
- les catégories restent locales à l'outil ; l'utilisateur peut ajouter plusieurs outils Combos pour séparer des usages.
|
||||
|
||||
Stockage compact :
|
||||
|
||||
- `device` est omis si la valeur vaut `playstation`.
|
||||
- `combos` est omis si aucun combo sans catégorie n'existe.
|
||||
- `categories` conserve uniquement les catégories avec au moins un combo valide.
|
||||
- `category` est omise si vide.
|
||||
- `categoryOrder` est omis si aucune catégorie n'est utilisée.
|
||||
- `collapsedCategories` est omis si aucune catégorie n'est repliée.
|
||||
- `combos` est omis si aucun combo valide n'existe.
|
||||
- les combos sans nom et sans touche sont supprimés.
|
||||
- `collapsed` est omis si `false`.
|
||||
- les titres de catégorie et noms de combo sont limités à 80 caractères.
|
||||
- les catégories et noms de combo sont limités à 80 caractères.
|
||||
- chaque combo conserve au maximum 64 étapes.
|
||||
|
||||
## Outil Calculateur
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.combos.customKeyPlaceholder",
|
||||
"toolboxes.modules.combos.addCustomKeyButton",
|
||||
"toolboxes.modules.combos.emptyCombos",
|
||||
"toolboxes.modules.combos.emptyCategory",
|
||||
"toolboxes.modules.combos.emptySequence",
|
||||
"toolboxes.modules.combos.showCategoryTitle",
|
||||
"toolboxes.modules.combos.hideCategoryTitle",
|
||||
|
|
|
|||
|
|
@ -99,23 +99,19 @@ test("combos storage normalizes devices, steps and compact export ids", () => {
|
|||
const longText = "x".repeat(120);
|
||||
const normalized = normalizeCombosData({
|
||||
device: "invalid",
|
||||
categoryOrder: [`${longText} `, "Missing", `${longText} `],
|
||||
collapsedCategories: [`${longText} `, "", `${longText} `],
|
||||
combos: [
|
||||
{
|
||||
id: "rootCombo",
|
||||
name: "Root",
|
||||
device: "switch",
|
||||
inputs: [[{ kind: "button", value: "triangle" }]]
|
||||
}
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
id: "category1",
|
||||
title: `${longText} `,
|
||||
collapsed: true,
|
||||
combos: [
|
||||
},
|
||||
{
|
||||
id: "combo1",
|
||||
device: "n64",
|
||||
category: `${longText} `,
|
||||
name: `${longText} `,
|
||||
inputs: [
|
||||
[
|
||||
|
|
@ -129,22 +125,17 @@ test("combos storage normalizes devices, steps and compact export ids", () => {
|
|||
},
|
||||
{ id: "empty", name: " ", inputs: [] }
|
||||
]
|
||||
},
|
||||
{ id: "emptyCategory", title: " ", combos: [] },
|
||||
{ id: "titledEmptyCategory", title: "Empty", combos: [] }
|
||||
]
|
||||
});
|
||||
|
||||
assert.equal(normalized.device, "playstation");
|
||||
assert.equal(normalized.combos.length, 1);
|
||||
assert.equal(normalized.combos.length, 2);
|
||||
assert.equal(normalized.combos[0].device, "switch");
|
||||
assert.equal(normalized.categories.length, 1);
|
||||
assert.equal(normalized.categories[0].title.length, 80);
|
||||
assert.equal(normalized.categories[0].collapsed, true);
|
||||
assert.equal(normalized.categories[0].combos.length, 1);
|
||||
assert.equal(normalized.categories[0].combos[0].name.length, 80);
|
||||
assert.equal("note" in normalized.categories[0].combos[0], false);
|
||||
assert.deepEqual(normalized.categories[0].combos[0].inputs, [
|
||||
assert.equal(normalized.combos[1].category.length, 80);
|
||||
assert.equal(normalized.combos[1].name.length, 80);
|
||||
assert.equal("note" in normalized.combos[1], false);
|
||||
assert.deepEqual(normalized.categoryOrder, [longText.slice(0, 80)]);
|
||||
assert.deepEqual(normalized.collapsedCategories, [longText.slice(0, 80)]);
|
||||
assert.deepEqual(normalized.combos[1].inputs, [
|
||||
[
|
||||
{ kind: "direction", value: "down" },
|
||||
{ kind: "button", value: "cross" }
|
||||
|
|
@ -154,11 +145,13 @@ test("combos storage normalizes devices, steps and compact export ids", () => {
|
|||
|
||||
const compact = compactModuleDataForStorage("combos", { ...normalized, device: "xbox" });
|
||||
assert.equal(compact.device, "xbox");
|
||||
assert.deepEqual(compact.categoryOrder, [longText.slice(0, 80)]);
|
||||
assert.deepEqual(compact.collapsedCategories, [longText.slice(0, 80)]);
|
||||
assert.equal(compact.combos[0].id, "rootCombo");
|
||||
assert.equal(compact.combos[0].device, "switch");
|
||||
assert.equal(compact.categories[0].id, "category1");
|
||||
assert.equal(compact.categories[0].combos[0].device, "n64");
|
||||
assert.equal(compact.categories[0].combos[0].id, "combo1");
|
||||
assert.equal(compact.combos[1].device, "n64");
|
||||
assert.equal(compact.combos[1].id, "combo1");
|
||||
assert.equal(compact.combos[1].category.length, 80);
|
||||
|
||||
const exported = createToolboxExportPayload(
|
||||
{ id: "toolbox1", name: "Combos", modules: [{ id: "module1", type: "combos" }], updatedAt: "2026-01-01T00:00:00.000Z" },
|
||||
|
|
@ -166,9 +159,10 @@ test("combos storage normalizes devices, steps and compact export ids", () => {
|
|||
);
|
||||
assert.equal(exported.modules.m1.combos[0].id, "o1");
|
||||
assert.equal(exported.modules.m1.combos[0].device, "switch");
|
||||
assert.equal(exported.modules.m1.categories[0].id, "g1");
|
||||
assert.equal(exported.modules.m1.categories[0].combos[0].id, "o2");
|
||||
assert.equal(exported.modules.m1.categories[0].combos[0].device, "n64");
|
||||
assert.deepEqual(exported.modules.m1.categoryOrder, [longText.slice(0, 80)]);
|
||||
assert.deepEqual(exported.modules.m1.collapsedCategories, [longText.slice(0, 80)]);
|
||||
assert.equal(exported.modules.m1.combos[1].id, "o2");
|
||||
assert.equal(exported.modules.m1.combos[1].device, "n64");
|
||||
});
|
||||
|
||||
test("notepad data migrates text, sanitizes html and compacts drawings", () => {
|
||||
|
|
|
|||
|
|
@ -312,7 +312,6 @@
|
|||
"customKeyPlaceholder": "Touche personnalisée",
|
||||
"addCustomKeyButton": "Ajouter touche",
|
||||
"emptyCombos": "Aucun combo enregistré.",
|
||||
"emptyCategory": "Aucun combo dans cette catégorie.",
|
||||
"emptySequence": "Combo sans touches",
|
||||
"showCategoryTitle": "Afficher la catégorie",
|
||||
"hideCategoryTitle": "Réduire la catégorie",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { useEffect, useMemo, useState } from "react";
|
|||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
||||
|
||||
const UNCATEGORIZED_DROP_ID = "combos:uncategorized";
|
||||
const CATEGORY_SECTION_PREFIX = "combos:category";
|
||||
const CATEGORY_BOUNDARY_DROP_PREFIX = "combos:category-boundary";
|
||||
|
||||
const DEVICE_OPTIONS = [
|
||||
{ value: "playstation", label: "PlayStation" },
|
||||
{ value: "xbox", label: "Xbox" },
|
||||
|
|
@ -105,13 +109,15 @@ function createInput(kind, value) {
|
|||
return { kind, value };
|
||||
}
|
||||
|
||||
function createCombo(context, id, name, inputs, fallbackName, device) {
|
||||
return {
|
||||
function createCombo(context, id, name, category, inputs, fallbackName, device) {
|
||||
const combo = {
|
||||
id: id || context.uid("combo"),
|
||||
name: name.trim() || fallbackName,
|
||||
device,
|
||||
inputs: inputs.map((step) => step.map((input) => ({ ...input }))).filter((step) => step.length)
|
||||
};
|
||||
if (category.trim()) combo.category = category.trim();
|
||||
return combo;
|
||||
}
|
||||
|
||||
function normalizeCustomKey(value) {
|
||||
|
|
@ -132,43 +138,361 @@ function getPaletteColumns(groups) {
|
|||
];
|
||||
}
|
||||
|
||||
function moveArrayItem(items, fromId, toId, placement, getId = (item) => item.id) {
|
||||
if (!fromId || !toId || fromId === toId) return items;
|
||||
const nextItems = [...items];
|
||||
const fromIndex = nextItems.findIndex((item) => getId(item) === fromId);
|
||||
const toIndex = nextItems.findIndex((item) => getId(item) === toId);
|
||||
if (fromIndex < 0 || toIndex < 0) return items;
|
||||
const [moved] = nextItems.splice(fromIndex, 1);
|
||||
const targetIndex = nextItems.findIndex((item) => getId(item) === toId);
|
||||
nextItems.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, moved);
|
||||
return nextItems;
|
||||
function getComboCategory(combo) {
|
||||
return String(combo?.category || "").trim();
|
||||
}
|
||||
|
||||
function removeComboFromData(data, comboId) {
|
||||
let movedCombo = null;
|
||||
const combos = data.combos.filter((combo) => {
|
||||
if (combo.id !== comboId) return true;
|
||||
movedCombo = combo;
|
||||
return false;
|
||||
function getCategoryGroups(combos, categoryOrder) {
|
||||
const categories = [];
|
||||
combos.forEach((combo) => {
|
||||
const category = getComboCategory(combo);
|
||||
if (category && !categories.includes(category)) categories.push(category);
|
||||
});
|
||||
const categories = data.categories.map((category) => ({
|
||||
...category,
|
||||
combos: category.combos.filter((combo) => {
|
||||
if (combo.id !== comboId) return true;
|
||||
movedCombo = combo;
|
||||
return false;
|
||||
})
|
||||
}));
|
||||
return { movedCombo, data: { ...data, combos, categories } };
|
||||
const orderedCategories = [
|
||||
...categoryOrder.filter((category) => categories.includes(category)),
|
||||
...categories.filter((category) => !categoryOrder.includes(category))
|
||||
];
|
||||
const groups = orderedCategories.map((category) => ({
|
||||
category,
|
||||
combos: combos.filter((combo) => getComboCategory(combo) === category)
|
||||
})).filter((group) => group.combos.length);
|
||||
const renderedCategories = new Set();
|
||||
return combos.map((combo) => {
|
||||
const category = getComboCategory(combo);
|
||||
if (!category) return { type: "combo", combo };
|
||||
if (renderedCategories.has(category)) return null;
|
||||
renderedCategories.add(category);
|
||||
return { type: "category", group: groups.find((group) => group.category === category) };
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function insertComboInList(list, combo, targetComboId = "", placement = "after") {
|
||||
if (!targetComboId) return [...list, combo];
|
||||
const targetIndex = list.findIndex((item) => item.id === targetComboId);
|
||||
if (targetIndex < 0) return [...list, combo];
|
||||
const nextList = [...list];
|
||||
nextList.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, combo);
|
||||
return nextList;
|
||||
function getCategoryComboIds(combos, category) {
|
||||
return combos.filter((combo) => getComboCategory(combo) === category).map((combo) => combo.id);
|
||||
}
|
||||
|
||||
function getAllCategories(combos) {
|
||||
const categories = [];
|
||||
combos.forEach((combo) => {
|
||||
const category = getComboCategory(combo);
|
||||
if (category && !categories.includes(category)) categories.push(category);
|
||||
});
|
||||
return categories;
|
||||
}
|
||||
|
||||
function getCompleteCategoryOrder(categoryOrder, categories) {
|
||||
return [
|
||||
...categoryOrder.filter((category) => categories.includes(category)),
|
||||
...categories.filter((category) => !categoryOrder.includes(category))
|
||||
];
|
||||
}
|
||||
|
||||
function getCategorySectionId(category) {
|
||||
return `${CATEGORY_SECTION_PREFIX}\n${category}`;
|
||||
}
|
||||
|
||||
function parseCategorySectionId(id) {
|
||||
const parts = String(id || "").split("\n");
|
||||
if (parts[0] !== CATEGORY_SECTION_PREFIX) return "";
|
||||
return parts.slice(1).join("\n");
|
||||
}
|
||||
|
||||
function isCategorySectionId(id) {
|
||||
return String(id || "").startsWith(`${CATEGORY_SECTION_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function getCategoryBoundaryDropId(category, placement) {
|
||||
return `${CATEGORY_BOUNDARY_DROP_PREFIX}\n${category}\n${placement}`;
|
||||
}
|
||||
|
||||
function parseCategoryBoundaryDropId(id) {
|
||||
const parts = String(id || "").split("\n");
|
||||
if (parts[0] !== CATEGORY_BOUNDARY_DROP_PREFIX) return { category: "", placement: "" };
|
||||
return {
|
||||
category: parts[1] || "",
|
||||
placement: parts[2] === "after" ? "after" : "before"
|
||||
};
|
||||
}
|
||||
|
||||
function isCategoryBoundaryDropId(id) {
|
||||
return String(id || "").startsWith(`${CATEGORY_BOUNDARY_DROP_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function isUncategorizedDropId(id) {
|
||||
return id === UNCATEGORIZED_DROP_ID;
|
||||
}
|
||||
|
||||
function moveComboGroup(combos, movingIds, targetComboId, placement = "before") {
|
||||
const movingSet = new Set(movingIds);
|
||||
if (!movingSet.size || movingSet.has(targetComboId)) return combos;
|
||||
const movingCombos = combos.filter((combo) => movingSet.has(combo.id));
|
||||
const remainingCombos = combos.filter((combo) => !movingSet.has(combo.id));
|
||||
const targetIndex = remainingCombos.findIndex((combo) => combo.id === targetComboId);
|
||||
if (targetIndex < 0) return combos;
|
||||
remainingCombos.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, ...movingCombos);
|
||||
return remainingCombos;
|
||||
}
|
||||
|
||||
function getBoundaryComboId(combos, comboIds, placement) {
|
||||
const ids = new Set(comboIds);
|
||||
const orderedCombos = combos.filter((combo) => ids.has(combo.id));
|
||||
return placement === "after" ? orderedCombos.at(-1)?.id || "" : orderedCombos[0]?.id || "";
|
||||
}
|
||||
|
||||
function moveCategoryOrder(categoryOrder, categories, fromCategory, toCategory, placement = "before") {
|
||||
const nextOrder = getCompleteCategoryOrder(categoryOrder, categories);
|
||||
const index = nextOrder.indexOf(fromCategory);
|
||||
const targetIndex = nextOrder.indexOf(toCategory);
|
||||
if (index < 0 || targetIndex < 0 || fromCategory === toCategory) return nextOrder;
|
||||
const [moved] = nextOrder.splice(index, 1);
|
||||
const nextTargetIndex = nextOrder.indexOf(toCategory);
|
||||
nextOrder.splice(placement === "after" ? nextTargetIndex + 1 : nextTargetIndex, 0, moved);
|
||||
return nextOrder;
|
||||
}
|
||||
|
||||
function moveCategoryOrderToStart(categoryOrder, categories, fromCategory) {
|
||||
const nextOrder = getCompleteCategoryOrder(categoryOrder, categories).filter((category) => category !== fromCategory);
|
||||
return [fromCategory, ...nextOrder];
|
||||
}
|
||||
|
||||
function moveCategoryOrderToEnd(categoryOrder, categories, fromCategory) {
|
||||
const nextOrder = getCompleteCategoryOrder(categoryOrder, categories).filter((category) => category !== fromCategory);
|
||||
return [...nextOrder, fromCategory];
|
||||
}
|
||||
|
||||
function moveComboToCategory(combos, comboId, category) {
|
||||
return combos.map((combo) => {
|
||||
if (combo.id !== comboId) return combo;
|
||||
const nextCombo = { ...combo };
|
||||
if (category) nextCombo.category = category;
|
||||
else delete nextCombo.category;
|
||||
return nextCombo;
|
||||
});
|
||||
}
|
||||
|
||||
function getFirstComboIdOutsideCategory(combos, category, excludedIds = []) {
|
||||
const excluded = new Set(excludedIds);
|
||||
return combos.find((combo) => !excluded.has(combo.id) && getComboCategory(combo) !== category)?.id || "";
|
||||
}
|
||||
|
||||
function getLastComboIdOutsideCategory(combos, category, excludedIds = []) {
|
||||
const excluded = new Set(excludedIds);
|
||||
return [...combos].reverse().find((combo) => !excluded.has(combo.id) && getComboCategory(combo) !== category)?.id || "";
|
||||
}
|
||||
|
||||
function insertCategoryGroupAtBoundary(data, fromCategory, boundary, movingIds) {
|
||||
const allCategories = getAllCategories(data.combos);
|
||||
const targetIds = getCategoryComboIds(data.combos, boundary.category);
|
||||
const boundaryComboId = getBoundaryComboId(data.combos, targetIds, boundary.placement);
|
||||
if (!boundaryComboId) return data;
|
||||
return {
|
||||
...data,
|
||||
combos: moveComboGroup(data.combos, movingIds, boundaryComboId, boundary.placement),
|
||||
categoryOrder: moveCategoryOrder(data.categoryOrder, allCategories, fromCategory, boundary.category, boundary.placement)
|
||||
};
|
||||
}
|
||||
|
||||
function moveCategoryGroupToEdge(data, fromCategory, edge) {
|
||||
const movingIds = getCategoryComboIds(data.combos, fromCategory);
|
||||
if (!movingIds.length) return data;
|
||||
const allCategories = getAllCategories(data.combos);
|
||||
const boundaryComboId = edge === "start"
|
||||
? getFirstComboIdOutsideCategory(data.combos, fromCategory, movingIds)
|
||||
: getLastComboIdOutsideCategory(data.combos, fromCategory, movingIds);
|
||||
const combos = boundaryComboId
|
||||
? moveComboGroup(data.combos, movingIds, boundaryComboId, edge === "start" ? "before" : "after")
|
||||
: data.combos;
|
||||
return {
|
||||
...data,
|
||||
combos,
|
||||
categoryOrder: edge === "start"
|
||||
? moveCategoryOrderToStart(data.categoryOrder, allCategories, fromCategory)
|
||||
: moveCategoryOrderToEnd(data.categoryOrder, allCategories, fromCategory)
|
||||
};
|
||||
}
|
||||
|
||||
function moveComboToEdge(data, comboId, edge, category = "") {
|
||||
const nextCombosWithCategory = moveComboToCategory(data.combos, comboId, category);
|
||||
const boundaryComboId = edge === "start"
|
||||
? nextCombosWithCategory.find((combo) => combo.id !== comboId)?.id || ""
|
||||
: [...nextCombosWithCategory].reverse().find((combo) => combo.id !== comboId)?.id || "";
|
||||
const combos = boundaryComboId
|
||||
? moveComboGroup(nextCombosWithCategory, [comboId], boundaryComboId, edge === "start" ? "before" : "after")
|
||||
: nextCombosWithCategory;
|
||||
return { ...data, combos };
|
||||
}
|
||||
|
||||
function moveComboBeforeCategoryBoundary(data, comboId, boundary) {
|
||||
const nextCombosWithCategory = moveComboToCategory(data.combos, comboId, "");
|
||||
const targetIds = getCategoryComboIds(data.combos, boundary.category).filter((id) => id !== comboId);
|
||||
const boundaryComboId = getBoundaryComboId(data.combos, targetIds, boundary.placement);
|
||||
return {
|
||||
...data,
|
||||
combos: boundaryComboId ? moveComboGroup(nextCombosWithCategory, [comboId], boundaryComboId, boundary.placement) : nextCombosWithCategory
|
||||
};
|
||||
}
|
||||
|
||||
function addCategoryToOrder(categoryOrder, category) {
|
||||
return category && !categoryOrder.includes(category) ? [...categoryOrder, category] : categoryOrder;
|
||||
}
|
||||
|
||||
function cleanCategoryOrder(categoryOrder, combos) {
|
||||
const categories = getAllCategories(combos);
|
||||
return getCompleteCategoryOrder(categoryOrder, categories);
|
||||
}
|
||||
|
||||
function getCategoryEdge(categoryDropTarget, comboDropTarget, category, edge) {
|
||||
const dropId = getCategoryBoundaryDropId(category, edge === "start" ? "before" : "after");
|
||||
return categoryDropTarget.id === dropId || comboDropTarget.id === dropId;
|
||||
}
|
||||
|
||||
function getDropEdgeClass(categoryDropTarget, comboDropTarget, category, edge) {
|
||||
return getCategoryEdge(categoryDropTarget, comboDropTarget, category, edge) ? "is-drop-target" : "";
|
||||
}
|
||||
|
||||
function getEdgeFromPlacement(placement) {
|
||||
return placement === "after" ? "end" : "start";
|
||||
}
|
||||
|
||||
function insertComboIntoCategory(data, comboId, category, placement) {
|
||||
const nextCombos = moveComboToCategory(data.combos, comboId, category);
|
||||
const targetIds = getCategoryComboIds(data.combos, category).filter((id) => id !== comboId);
|
||||
const boundaryComboId = getBoundaryComboId(data.combos, targetIds, placement);
|
||||
return {
|
||||
...data,
|
||||
combos: boundaryComboId ? moveComboGroup(nextCombos, [comboId], boundaryComboId, placement) : nextCombos,
|
||||
categoryOrder: addCategoryToOrder(data.categoryOrder, category),
|
||||
collapsedCategories: data.collapsedCategories.filter((item) => item !== category)
|
||||
};
|
||||
}
|
||||
|
||||
function moveCategoryGroupAroundCombo(data, fromCategory, targetComboId, placement, movingIds) {
|
||||
return {
|
||||
...data,
|
||||
combos: moveComboGroup(data.combos, movingIds, targetComboId, placement)
|
||||
};
|
||||
}
|
||||
|
||||
function getCategoryOrderAfterComboMove(categoryOrder, combos) {
|
||||
return cleanCategoryOrder(categoryOrder, combos);
|
||||
}
|
||||
|
||||
function moveComboAroundCombo(data, fromComboId, targetComboId, placement) {
|
||||
const targetCombo = data.combos.find((combo) => combo.id === targetComboId);
|
||||
if (!targetCombo) return data;
|
||||
const targetCategory = getComboCategory(targetCombo);
|
||||
const nextCombos = moveComboToCategory(data.combos, fromComboId, targetCategory);
|
||||
const movedCombos = moveComboGroup(nextCombos, [fromComboId], targetComboId, placement);
|
||||
return {
|
||||
...data,
|
||||
combos: movedCombos,
|
||||
categoryOrder: getCategoryOrderAfterComboMove(data.categoryOrder, movedCombos)
|
||||
};
|
||||
}
|
||||
|
||||
function isBoundaryEdgeTarget(toId, edge) {
|
||||
if (!isCategoryBoundaryDropId(toId)) return false;
|
||||
return parseCategoryBoundaryDropId(toId).placement === (edge === "start" ? "before" : "after");
|
||||
}
|
||||
|
||||
function moveCategoryBoundary(data, fromCategory, toId) {
|
||||
if (isUncategorizedDropId(toId)) return moveCategoryGroupToEdge(data, fromCategory, "start");
|
||||
if (!isCategoryBoundaryDropId(toId)) return data;
|
||||
const boundary = parseCategoryBoundaryDropId(toId);
|
||||
if (!boundary.category || boundary.category === fromCategory) return data;
|
||||
if (isBoundaryEdgeTarget(toId, "start") && getCompleteCategoryOrder(data.categoryOrder, getAllCategories(data.combos))[0] === boundary.category) {
|
||||
return moveCategoryGroupToEdge(data, fromCategory, "start");
|
||||
}
|
||||
if (isBoundaryEdgeTarget(toId, "end")) {
|
||||
const order = getCompleteCategoryOrder(data.categoryOrder, getAllCategories(data.combos));
|
||||
if (order.at(-1) === boundary.category) return moveCategoryGroupToEdge(data, fromCategory, "end");
|
||||
}
|
||||
const movingIds = getCategoryComboIds(data.combos, fromCategory);
|
||||
return insertCategoryGroupAtBoundary(data, fromCategory, boundary, movingIds);
|
||||
}
|
||||
|
||||
function moveCategoryOrderAroundTarget(categoryOrder, combos, fromCategory, toCategory, placement) {
|
||||
return moveCategoryOrder(categoryOrder, getAllCategories(combos), fromCategory, toCategory, placement);
|
||||
}
|
||||
|
||||
function moveCategoryAroundCategory(data, fromCategory, toCategory, placement) {
|
||||
const movingIds = getCategoryComboIds(data.combos, fromCategory);
|
||||
const targetIds = getCategoryComboIds(data.combos, toCategory);
|
||||
const boundaryComboId = getBoundaryComboId(data.combos, targetIds, placement);
|
||||
if (!boundaryComboId) return data;
|
||||
return {
|
||||
...data,
|
||||
combos: moveComboGroup(data.combos, movingIds, boundaryComboId, placement),
|
||||
categoryOrder: moveCategoryOrderAroundTarget(data.categoryOrder, data.combos, fromCategory, toCategory, placement)
|
||||
};
|
||||
}
|
||||
|
||||
function removeComboCategoryAtBoundary(data, comboId, boundary) {
|
||||
const nextData = moveComboBeforeCategoryBoundary(data, comboId, boundary);
|
||||
return {
|
||||
...nextData,
|
||||
categoryOrder: cleanCategoryOrder(nextData.categoryOrder, nextData.combos)
|
||||
};
|
||||
}
|
||||
|
||||
function moveComboByBoundary(data, fromComboId, toId) {
|
||||
const boundary = parseCategoryBoundaryDropId(toId);
|
||||
if (!boundary.category) return data;
|
||||
return removeComboCategoryAtBoundary(data, fromComboId, boundary);
|
||||
}
|
||||
|
||||
function removeComboCategory(data, comboId, placement) {
|
||||
return moveComboToEdge(data, comboId, getEdgeFromPlacement(placement), "");
|
||||
}
|
||||
|
||||
function insertAfterCategory(data, comboId, category, placement) {
|
||||
return insertComboIntoCategory(data, comboId, category, placement);
|
||||
}
|
||||
|
||||
function getCategoryTargetId(target) {
|
||||
if (target.dataset.dropId) return target.dataset.dropId;
|
||||
if (target.classList.contains("combos-category")) return getCategorySectionId(target.dataset.category || "");
|
||||
return target.dataset.comboId || "";
|
||||
}
|
||||
|
||||
function getComboTargetId(target) {
|
||||
if (target.dataset.dropId) return target.dataset.dropId;
|
||||
if (target.classList.contains("combo-card")) return target.dataset.comboId || "";
|
||||
return getCategorySectionId(target.dataset.category || "");
|
||||
}
|
||||
|
||||
function canDropComboOnTarget(target, draggingId, combos) {
|
||||
const draggingCombo = combos.find((combo) => combo.id === draggingId);
|
||||
if (!draggingCombo) return false;
|
||||
const draggingCategory = getComboCategory(draggingCombo);
|
||||
if (target.classList.contains("combos-root-drop-zone")) return Boolean(draggingCategory);
|
||||
if (target.classList.contains("combos-category-boundary-drop-zone")) return true;
|
||||
if (target.classList.contains("combos-category")) return target.dataset.category !== draggingCategory;
|
||||
if (target.classList.contains("combo-card")) return target.dataset.comboId !== draggingId;
|
||||
return false;
|
||||
}
|
||||
|
||||
function canDropCategoryOnTarget(target, draggingId) {
|
||||
const draggingCategory = parseCategorySectionId(draggingId);
|
||||
if (!draggingCategory) return false;
|
||||
if (target.classList.contains("combos-root-drop-zone")) return true;
|
||||
if (target.classList.contains("combos-category-boundary-drop-zone")) {
|
||||
const boundary = parseCategoryBoundaryDropId(target.dataset.dropId);
|
||||
return boundary.category && boundary.category !== draggingCategory;
|
||||
}
|
||||
if (target.classList.contains("combos-category")) return target.dataset.category && target.dataset.category !== draggingCategory;
|
||||
return !target.dataset.comboCategory;
|
||||
}
|
||||
|
||||
function getCategoryDropPlacement(event, target) {
|
||||
if (target.classList.contains("combos-category-boundary-drop-zone") || target.classList.contains("combos-root-drop-zone")) return "before";
|
||||
const rect = target.getBoundingClientRect();
|
||||
return event.clientY > rect.top + rect.height / 2 ? "after" : "before";
|
||||
}
|
||||
|
||||
function getComboDropPlacement(event, target) {
|
||||
if (target.classList.contains("combos-category-boundary-drop-zone") || target.classList.contains("combos-root-drop-zone")) return "before";
|
||||
const rect = target.getBoundingClientRect();
|
||||
return event.clientY > rect.top + rect.height / 2 ? "after" : "before";
|
||||
}
|
||||
|
||||
function ComboInputToken({ input, device, palette = false, onClick, onDragStart }) {
|
||||
|
|
@ -292,7 +616,7 @@ function InlineTextInput({ value, onCommit, className, ariaLabel, placeholder =
|
|||
}
|
||||
|
||||
export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [], categories: [] }));
|
||||
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [] }));
|
||||
const textContent = context.moduleText?.combos || {};
|
||||
const [comboCategory, setComboCategory] = useState("");
|
||||
const [comboName, setComboName] = useState("");
|
||||
|
|
@ -305,13 +629,16 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
const paletteGroups = useMemo(() => getPaletteGroups(data.device, keyboardLayout), [data.device, keyboardLayout]);
|
||||
const paletteColumns = useMemo(() => getPaletteColumns(paletteGroups), [paletteGroups]);
|
||||
const cleanDraftInputs = draftInputs.map((step) => step.filter(Boolean)).filter((step) => step.length);
|
||||
const entries = getCategoryGroups(data.combos, data.categoryOrder);
|
||||
const {
|
||||
draggingId: draggingCategoryId,
|
||||
dropTarget: categoryDropTarget,
|
||||
startDrag: startCategoryDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".combos-category",
|
||||
getTargetId: (target) => target.dataset.categoryId,
|
||||
targetSelector: ".combos-category, .combo-card, .combos-root-drop-zone, .combos-category-boundary-drop-zone",
|
||||
getTargetId: getCategoryTargetId,
|
||||
canDropOn: canDropCategoryOnTarget,
|
||||
getPlacement: getCategoryDropPlacement,
|
||||
onMove: moveCategory
|
||||
});
|
||||
const {
|
||||
|
|
@ -319,17 +646,10 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
dropTarget: comboDropTarget,
|
||||
startDrag: startComboDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".combo-card, .combos-root-drop-zone, .combos-category",
|
||||
getTargetId: (target) => {
|
||||
if (target.classList.contains("combo-card")) return `combo:${target.dataset.comboId}`;
|
||||
if (target.classList.contains("combos-category")) return `category:${target.dataset.categoryId}`;
|
||||
return "root";
|
||||
},
|
||||
canDropOn: (target, draggingId) => {
|
||||
if (target.classList.contains("combo-card")) return target.dataset.comboId !== draggingId;
|
||||
if (target.classList.contains("combos-category")) return !target.querySelector(`[data-combo-id="${draggingId}"]`);
|
||||
return true;
|
||||
},
|
||||
targetSelector: ".combo-card, .combos-category, .combos-root-drop-zone, .combos-category-boundary-drop-zone",
|
||||
getTargetId: getComboTargetId,
|
||||
canDropOn: (target, draggingId) => canDropComboOnTarget(target, draggingId, data.combos),
|
||||
getPlacement: getComboDropPlacement,
|
||||
onMove: moveCombo
|
||||
});
|
||||
|
||||
|
|
@ -342,64 +662,43 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
save({
|
||||
...data,
|
||||
device,
|
||||
combos: data.combos.map(stampComboDevice),
|
||||
categories: data.categories.map((category) => ({
|
||||
...category,
|
||||
combos: category.combos.map(stampComboDevice)
|
||||
}))
|
||||
combos: data.combos.map(stampComboDevice)
|
||||
});
|
||||
}
|
||||
|
||||
function moveCategory(fromCategoryId, toCategoryId, placement) {
|
||||
save({ ...data, categories: moveArrayItem(data.categories, fromCategoryId, toCategoryId, placement) });
|
||||
function moveCategory(fromId, toId, placement) {
|
||||
const fromCategory = parseCategorySectionId(fromId);
|
||||
const movingIds = getCategoryComboIds(data.combos, fromCategory);
|
||||
if (!movingIds.length) return;
|
||||
if (isUncategorizedDropId(toId) || isCategoryBoundaryDropId(toId)) {
|
||||
save(moveCategoryBoundary(data, fromCategory, toId));
|
||||
return;
|
||||
}
|
||||
if (isCategorySectionId(toId)) {
|
||||
save(moveCategoryAroundCategory(data, fromCategory, parseCategorySectionId(toId), placement));
|
||||
return;
|
||||
}
|
||||
if (data.combos.some((combo) => combo.id === toId)) {
|
||||
save(moveCategoryGroupAroundCombo(data, fromCategory, toId, placement, movingIds));
|
||||
}
|
||||
}
|
||||
|
||||
function moveCombo(fromComboId, targetId, placement) {
|
||||
const { movedCombo, data: dataWithoutCombo } = removeComboFromData(data, fromComboId);
|
||||
const movedCombo = data.combos.find((combo) => combo.id === fromComboId);
|
||||
if (!movedCombo) return;
|
||||
if (targetId === "root") {
|
||||
save({ ...dataWithoutCombo, combos: [...dataWithoutCombo.combos, movedCombo] });
|
||||
if (isUncategorizedDropId(targetId)) {
|
||||
save(removeComboCategory(data, fromComboId, placement));
|
||||
return;
|
||||
}
|
||||
if (targetId.startsWith("category:")) {
|
||||
const categoryId = targetId.replace("category:", "");
|
||||
save({
|
||||
...dataWithoutCombo,
|
||||
categories: dataWithoutCombo.categories.map((category) => category.id === categoryId
|
||||
? { ...category, combos: [...category.combos, movedCombo], collapsed: false }
|
||||
: category)
|
||||
});
|
||||
if (isCategoryBoundaryDropId(targetId)) {
|
||||
save(moveComboByBoundary(data, fromComboId, targetId));
|
||||
return;
|
||||
}
|
||||
const targetComboId = targetId.replace("combo:", "");
|
||||
const targetElement = document.querySelector(`[data-combo-id="${targetComboId}"]`);
|
||||
const targetList = targetElement?.dataset.comboList || "";
|
||||
if (!targetList) {
|
||||
save({ ...dataWithoutCombo, combos: insertComboInList(dataWithoutCombo.combos, movedCombo, targetComboId, placement) });
|
||||
if (isCategorySectionId(targetId)) {
|
||||
save(insertAfterCategory(data, fromComboId, parseCategorySectionId(targetId), placement));
|
||||
return;
|
||||
}
|
||||
save({
|
||||
...dataWithoutCombo,
|
||||
categories: dataWithoutCombo.categories.map((category) => category.id === targetList
|
||||
? { ...category, combos: insertComboInList(category.combos, movedCombo, targetComboId, placement), collapsed: false }
|
||||
: category)
|
||||
});
|
||||
}
|
||||
|
||||
function updateCategory(categoryId, updater) {
|
||||
save({
|
||||
...data,
|
||||
categories: data.categories.map((category) => category.id === categoryId ? updater(category) : category)
|
||||
});
|
||||
}
|
||||
|
||||
function getCategoryTarget(categories, title) {
|
||||
const cleanTitle = title.trim();
|
||||
if (!cleanTitle) return { categoryId: "", categories };
|
||||
const existing = categories.find((category) => category.title.toLowerCase() === cleanTitle.toLowerCase());
|
||||
if (existing) return { categoryId: existing.id, categories };
|
||||
const category = { id: context.uid("section"), title: cleanTitle, collapsed: false, combos: [] };
|
||||
return { categoryId: category.id, categories: [...categories, category] };
|
||||
save(moveComboAroundCombo(data, fromComboId, targetId, placement));
|
||||
}
|
||||
|
||||
function addInputToStep(kind, value, stepIndex) {
|
||||
|
|
@ -534,26 +833,17 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
function saveCombo(event) {
|
||||
event.preventDefault();
|
||||
if (!comboName.trim() && !cleanDraftInputs.length) return;
|
||||
const combo = createCombo(context, editingComboId, comboName, cleanDraftInputs, textContent.defaultComboName || "Combo", data.device);
|
||||
const nextRootCombos = data.combos.filter((item) => item.id !== editingComboId);
|
||||
const nextCategories = data.categories.map((category) => ({
|
||||
...category,
|
||||
combos: category.combos.filter((item) => item.id !== editingComboId)
|
||||
}));
|
||||
const target = getCategoryTarget(nextCategories, comboCategory);
|
||||
const nextData = target.categoryId
|
||||
? {
|
||||
...data,
|
||||
combos: nextRootCombos,
|
||||
categories: target.categories.map((category) => category.id === target.categoryId ? { ...category, combos: [...category.combos, combo], collapsed: false } : category)
|
||||
}
|
||||
: { ...data, combos: [...nextRootCombos, combo], categories: nextCategories };
|
||||
save(nextData);
|
||||
const combo = createCombo(context, editingComboId, comboName, comboCategory, cleanDraftInputs, textContent.defaultComboName || "Combo", data.device);
|
||||
const nextCombos = data.combos.filter((item) => item.id !== editingComboId);
|
||||
const nextCategoryOrder = combo.category && !data.categoryOrder.includes(combo.category)
|
||||
? [...data.categoryOrder, combo.category]
|
||||
: data.categoryOrder;
|
||||
save({ ...data, combos: [...nextCombos, combo], categoryOrder: nextCategoryOrder });
|
||||
cancelComboEdit();
|
||||
}
|
||||
|
||||
function startComboEdit(categoryId, combo) {
|
||||
setComboCategory(data.categories.find((category) => category.id === categoryId)?.title || "");
|
||||
function startComboEdit(combo) {
|
||||
setComboCategory(getComboCategory(combo));
|
||||
setEditingComboId(combo.id);
|
||||
setComboName(combo.name);
|
||||
setDraftInputs(combo.inputs.length ? combo.inputs.map((step) => step.map((input) => ({ ...input }))) : [[]]);
|
||||
|
|
@ -569,32 +859,33 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
setSimultaneousMode(false);
|
||||
}
|
||||
|
||||
function updateCombo(categoryId, comboId, updater) {
|
||||
if (!categoryId) {
|
||||
function updateCombo(comboId, updater) {
|
||||
save({ ...data, combos: data.combos.map((combo) => combo.id === comboId ? updater(combo) : combo).filter((combo) => combo.name || combo.inputs.length) });
|
||||
return;
|
||||
}
|
||||
updateCategory(categoryId, (category) => ({
|
||||
...category,
|
||||
combos: category.combos.map((combo) => combo.id === comboId ? updater(combo) : combo).filter((combo) => combo.name || combo.inputs.length)
|
||||
}));
|
||||
}
|
||||
|
||||
function deleteCombo(categoryId, comboId) {
|
||||
updateCombo(categoryId, comboId, () => ({ name: "", inputs: [] }));
|
||||
function deleteCombo(comboId) {
|
||||
updateCombo(comboId, () => ({ name: "", inputs: [] }));
|
||||
if (editingComboId === comboId) cancelComboEdit();
|
||||
}
|
||||
|
||||
function renderCombo(combo, categoryId = "") {
|
||||
function toggleCategoryCollapsed(category) {
|
||||
const collapsedCategories = data.collapsedCategories.includes(category)
|
||||
? data.collapsedCategories.filter((item) => item !== category)
|
||||
: [...data.collapsedCategories, category];
|
||||
save({ ...data, collapsedCategories });
|
||||
}
|
||||
|
||||
function renderCombo(combo) {
|
||||
const category = getComboCategory(combo);
|
||||
const comboClassName = [
|
||||
"combo-card",
|
||||
"is-editing",
|
||||
draggingComboId === combo.id ? "is-dragging" : "",
|
||||
comboDropTarget.id === `combo:${combo.id}` ? "is-drop-target" : "",
|
||||
comboDropTarget.id === `combo:${combo.id}` && comboDropTarget.placement === "after" ? "drop-after" : ""
|
||||
comboDropTarget.id === combo.id || categoryDropTarget.id === combo.id ? "is-drop-target" : "",
|
||||
(comboDropTarget.id === combo.id && comboDropTarget.placement === "after") || (categoryDropTarget.id === combo.id && categoryDropTarget.placement === "after") ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<article className={comboClassName} key={combo.id} data-combo-id={combo.id} data-combo-list={categoryId}>
|
||||
<article className={comboClassName} key={combo.id} data-combo-id={combo.id} data-combo-category={category}>
|
||||
<button
|
||||
className="task-planner-drag-handle combo-drag-handle"
|
||||
type="button"
|
||||
|
|
@ -608,16 +899,16 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
<InlineTextInput
|
||||
className="tool-split-entry-label combo-title-input"
|
||||
value={combo.name}
|
||||
onCommit={(name) => updateCombo(categoryId, combo.id, (current) => ({ ...current, name: name || current.name }))}
|
||||
onCommit={(name) => updateCombo(combo.id, (current) => ({ ...current, name: name || current.name }))}
|
||||
ariaLabel={textContent.comboNameLabel || "Nom"}
|
||||
/>
|
||||
<ComboSequence inputs={combo.inputs} device={combo.device || data.device} emptyLabel={textContent.emptySequence || "Combo sans touches"} />
|
||||
</div>
|
||||
<div className="combo-card-actions">
|
||||
<button type="button" className="checklist-delete-button" onClick={() => startComboEdit(categoryId, combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
|
||||
<button type="button" className="checklist-delete-button" onClick={() => startComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button type="button" className="checklist-delete-button danger" onClick={() => deleteCombo(categoryId, combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
|
||||
<button type="button" className="checklist-delete-button danger" onClick={() => deleteCombo(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -625,50 +916,82 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
);
|
||||
}
|
||||
|
||||
function renderCategory(category) {
|
||||
function renderCategory(group) {
|
||||
const category = group.category;
|
||||
const categoryId = getCategorySectionId(category);
|
||||
const beforeDropId = getCategoryBoundaryDropId(category, "before");
|
||||
const afterDropId = getCategoryBoundaryDropId(category, "after");
|
||||
const isCollapsed = data.collapsedCategories.includes(category);
|
||||
const categoryClassName = [
|
||||
"combos-category checklist-section is-grouped",
|
||||
category.collapsed ? "is-collapsed" : "",
|
||||
draggingCategoryId === category.id ? "is-dragging" : "",
|
||||
categoryDropTarget.id === category.id || comboDropTarget.id === `category:${category.id}` ? "is-drop-target" : "",
|
||||
(categoryDropTarget.id === category.id && categoryDropTarget.placement === "after") ? "drop-after" : ""
|
||||
isCollapsed ? "is-collapsed" : "",
|
||||
draggingCategoryId === categoryId ? "is-dragging" : "",
|
||||
categoryDropTarget.id === categoryId || comboDropTarget.id === categoryId ? "is-drop-target" : "",
|
||||
(categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after") || (comboDropTarget.id === categoryId && comboDropTarget.placement === "after") ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
<section className={categoryClassName} key={category.id} data-category-id={category.id}>
|
||||
const showBoundaryDropZones = Boolean(draggingComboId || draggingCategoryId);
|
||||
const beforeDropZone = showBoundaryDropZones ? (
|
||||
<div
|
||||
className={`combos-category-boundary-drop-zone ${getDropEdgeClass(categoryDropTarget, comboDropTarget, category, "start")}`}
|
||||
key={`${category}:before-drop`}
|
||||
data-drop-id={beforeDropId}
|
||||
/>
|
||||
) : null;
|
||||
const afterDropZone = showBoundaryDropZones ? (
|
||||
<div
|
||||
className={`combos-category-boundary-drop-zone ${getDropEdgeClass(categoryDropTarget, comboDropTarget, category, "end")}`}
|
||||
key={`${category}:after-drop`}
|
||||
data-drop-id={afterDropId}
|
||||
/>
|
||||
) : null;
|
||||
return [
|
||||
beforeDropZone,
|
||||
<section className={categoryClassName} key={category} data-category={category}>
|
||||
<div className="checklist-section-header combos-category-header">
|
||||
<div className="combos-category-title">
|
||||
<button
|
||||
className="task-planner-category-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => startCategoryDrag(event, category.id)}
|
||||
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category.title}`}
|
||||
onPointerDown={(event) => startCategoryDrag(event, categoryId)}
|
||||
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category}`}
|
||||
title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<h3>{category.title}</h3>
|
||||
<h3>{category}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<span>{category.combos.length}</span>
|
||||
<span>{group.combos.length}</span>
|
||||
<button
|
||||
className="checklist-section-collapse-button"
|
||||
type="button"
|
||||
onClick={() => updateCategory(category.id, (item) => ({ ...item, collapsed: !item.collapsed }))}
|
||||
aria-expanded={!category.collapsed}
|
||||
aria-label={category.collapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||
title={category.collapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||
onClick={() => toggleCategoryCollapsed(category)}
|
||||
aria-expanded={!isCollapsed}
|
||||
aria-label={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||
title={isCollapsed ? textContent.showCategoryTitle || "Afficher la catégorie" : textContent.hideCategoryTitle || "Réduire la catégorie"}
|
||||
>
|
||||
<Icon name={category.collapsed ? "chevron-down" : "chevron-up"} />
|
||||
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!category.collapsed && (
|
||||
{!isCollapsed && (
|
||||
<div className="combos-list">
|
||||
{category.combos.map((combo) => renderCombo(combo, category.id))}
|
||||
{!category.combos.length && <p className="empty-state">{textContent.emptyCategory || "Aucun combo dans cette catégorie."}</p>}
|
||||
{group.combos.map((combo) => renderCombo(combo))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>,
|
||||
afterDropZone
|
||||
];
|
||||
}
|
||||
|
||||
function renderUncategorizedDropZone() {
|
||||
const draggingCombo = draggingComboId ? data.combos.find((combo) => combo.id === draggingComboId) : null;
|
||||
const draggingCategory = parseCategorySectionId(draggingCategoryId);
|
||||
if ((!draggingCombo || !getComboCategory(draggingCombo)) && !draggingCategory) return null;
|
||||
return (
|
||||
<div className={`combos-root-drop-zone is-visible ${comboDropTarget.id === UNCATEGORIZED_DROP_ID || categoryDropTarget.id === UNCATEGORIZED_DROP_ID ? "is-drop-target" : ""}`} data-drop-id={UNCATEGORIZED_DROP_ID}>
|
||||
{textContent.noCategoryLabel || "Sans catégorie"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -787,20 +1110,13 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!data.combos.length && !data.categories.length && (
|
||||
{!data.combos.length && (
|
||||
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
|
||||
)}
|
||||
|
||||
<div className="combos-category-list">
|
||||
<div className={`combos-root-drop-zone ${draggingComboId ? "is-visible" : ""} ${comboDropTarget.id === "root" ? "is-drop-target" : ""}`} data-combo-root-drop="true">
|
||||
{textContent.noCategoryLabel || "Sans catégorie"}
|
||||
</div>
|
||||
{data.combos.length > 0 && (
|
||||
<div className="combos-list combos-root-list">
|
||||
{data.combos.map((combo) => renderCombo(combo))}
|
||||
</div>
|
||||
)}
|
||||
{data.categories.map(renderCategory)}
|
||||
{renderUncategorizedDropZone()}
|
||||
{entries.map((entry) => entry.type === "category" ? renderCategory(entry.group) : renderCombo(entry.combo))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
|||
|
||||
const TASK_TYPES = ["daily", "weekly", "unique"];
|
||||
const UNCATEGORIZED_DROP_ID = "task-planner:uncategorized";
|
||||
const CATEGORY_BOUNDARY_DROP_PREFIX = "task-planner:category-boundary";
|
||||
const WEEK_DAYS = [
|
||||
{ value: 1, label: "Lundi" },
|
||||
{ value: 2, label: "Mardi" },
|
||||
|
|
@ -95,17 +96,38 @@ function getBoundaryTaskId(tasks, taskIds, placement) {
|
|||
return placement === "after" ? orderedTasks.at(-1)?.id || "" : orderedTasks[0]?.id || "";
|
||||
}
|
||||
|
||||
function moveCategory(categoryOrder, fromCategory, toCategory, placement = "before") {
|
||||
const index = categoryOrder.indexOf(fromCategory);
|
||||
const targetIndex = categoryOrder.indexOf(toCategory);
|
||||
if (index < 0 || targetIndex < 0 || fromCategory === toCategory) return categoryOrder;
|
||||
const nextOrder = [...categoryOrder];
|
||||
function getRootCategories(tasks, parentMap) {
|
||||
const categories = [];
|
||||
tasks.forEach((task) => {
|
||||
if (parentMap.get(task.id) || !getTaskCategory(task) || categories.includes(task.category)) return;
|
||||
categories.push(task.category);
|
||||
});
|
||||
return categories;
|
||||
}
|
||||
|
||||
function getCompleteCategoryOrder(categoryOrder, categories) {
|
||||
return [
|
||||
...categoryOrder.filter((category) => categories.includes(category)),
|
||||
...categories.filter((category) => !categoryOrder.includes(category))
|
||||
];
|
||||
}
|
||||
|
||||
function moveCategory(categoryOrder, categories, fromCategory, toCategory, placement = "before") {
|
||||
const nextOrder = getCompleteCategoryOrder(categoryOrder, categories);
|
||||
const index = nextOrder.indexOf(fromCategory);
|
||||
const targetIndex = nextOrder.indexOf(toCategory);
|
||||
if (index < 0 || targetIndex < 0 || fromCategory === toCategory) return nextOrder;
|
||||
const [moved] = nextOrder.splice(index, 1);
|
||||
const nextTargetIndex = nextOrder.indexOf(toCategory);
|
||||
nextOrder.splice(placement === "after" ? nextTargetIndex + 1 : nextTargetIndex, 0, moved);
|
||||
return nextOrder;
|
||||
}
|
||||
|
||||
function moveCategoryToStart(categoryOrder, categories, fromCategory) {
|
||||
const nextOrder = getCompleteCategoryOrder(categoryOrder, categories).filter((category) => category !== fromCategory);
|
||||
return [fromCategory, ...nextOrder.filter(Boolean)];
|
||||
}
|
||||
|
||||
function getTaskTitle(tasks, taskId) {
|
||||
return tasks.find((task) => task.id === taskId)?.title || "Tâche supprimée";
|
||||
}
|
||||
|
|
@ -257,12 +279,36 @@ function isUncategorizedDropId(id) {
|
|||
return id === UNCATEGORIZED_DROP_ID;
|
||||
}
|
||||
|
||||
function getCategoryBoundaryDropId(parentId, category, placement) {
|
||||
return `${CATEGORY_BOUNDARY_DROP_PREFIX}\n${parentId || ""}\n${category}\n${placement}`;
|
||||
}
|
||||
|
||||
function parseCategoryBoundaryDropId(id) {
|
||||
const parts = String(id || "").split("\n");
|
||||
if (parts[0] !== CATEGORY_BOUNDARY_DROP_PREFIX) return { parentId: "", category: "", placement: "" };
|
||||
return {
|
||||
parentId: parts[1] || "",
|
||||
category: parts[2] || "",
|
||||
placement: parts[3] === "after" ? "after" : "before"
|
||||
};
|
||||
}
|
||||
|
||||
function isCategoryBoundaryDropId(id) {
|
||||
return String(id || "").startsWith(`${CATEGORY_BOUNDARY_DROP_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function getRootTaskIdsForCategory(tasks, parentMap, category) {
|
||||
return tasks
|
||||
.filter((task) => !parentMap.get(task.id) && getTaskCategory(task) === category)
|
||||
.map((task) => task.id);
|
||||
}
|
||||
|
||||
function getRootTaskIds(tasks, parentMap) {
|
||||
return tasks
|
||||
.filter((task) => !parentMap.get(task.id))
|
||||
.map((task) => task.id);
|
||||
}
|
||||
|
||||
export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
||||
const textContent = context.moduleText?.taskPlanner || {};
|
||||
|
|
@ -280,13 +326,17 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
dropTarget,
|
||||
startDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".task-planner-item, .task-planner-category-section, .task-planner-uncategorized-drop-zone",
|
||||
targetSelector: ".task-planner-item, .task-planner-category-section, .task-planner-uncategorized-drop-zone, .task-planner-category-boundary-drop-zone",
|
||||
getTargetId: (target) => target.dataset.dropId || target.dataset.taskId || getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
|
||||
canDropOn: (target, draggingTaskId) => {
|
||||
const draggingTask = data.tasks.find((task) => task.id === draggingTaskId);
|
||||
if (!draggingTask) return false;
|
||||
const draggingParentId = parentMap.get(draggingTaskId) || "";
|
||||
const draggingCategory = getTaskCategoryDataset(draggingTask, data.tasks, parentMap);
|
||||
if (target.classList.contains("task-planner-category-boundary-drop-zone")) {
|
||||
const boundary = parseCategoryBoundaryDropId(target.dataset.dropId);
|
||||
return !draggingParentId && !boundary.parentId && Boolean(boundary.category);
|
||||
}
|
||||
if (target.classList.contains("task-planner-uncategorized-drop-zone")) {
|
||||
return !draggingParentId && Boolean(draggingCategory);
|
||||
}
|
||||
|
|
@ -321,8 +371,25 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
moveToCategory("");
|
||||
return;
|
||||
}
|
||||
if (isCategoryBoundaryDropId(toId)) {
|
||||
const boundary = parseCategoryBoundaryDropId(toId);
|
||||
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, boundary.category);
|
||||
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, boundary.placement);
|
||||
if (!boundaryTaskId) return;
|
||||
const sourceCategory = getTaskCategoryDataset(fromTask, data.tasks, parentMap);
|
||||
const tasks = sourceCategory ? setRootTaskCategory(data.tasks, "") : data.tasks;
|
||||
save({ ...data, tasks: moveTask(tasks, fromTaskId, boundaryTaskId, boundary.placement) });
|
||||
return;
|
||||
}
|
||||
if (isCategorySectionId(toId)) {
|
||||
const targetCategory = parseCategorySectionId(toId).category;
|
||||
const sourceCategory = getTaskCategoryDataset(fromTask, data.tasks, parentMap);
|
||||
if (!sourceCategory) {
|
||||
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, targetCategory);
|
||||
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, placement);
|
||||
if (boundaryTaskId) save({ ...data, tasks: moveTask(data.tasks, fromTaskId, boundaryTaskId, placement) });
|
||||
return;
|
||||
}
|
||||
moveToCategory(targetCategory);
|
||||
return;
|
||||
}
|
||||
|
|
@ -339,11 +406,17 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
dropTarget: categoryDropTarget,
|
||||
startDrag: startCategoryDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".task-planner-category-section, .task-planner-item",
|
||||
getTargetId: (target) => target.dataset.taskId || getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
|
||||
targetSelector: ".task-planner-category-section, .task-planner-item, .task-planner-uncategorized-drop-zone, .task-planner-category-boundary-drop-zone",
|
||||
getTargetId: (target) => target.dataset.dropId || target.dataset.taskId || getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""),
|
||||
canDropOn: (target, draggingIdValue) => {
|
||||
const draggingCategory = parseCategorySectionId(draggingIdValue);
|
||||
if (!draggingCategory.category || target.dataset.parentId !== draggingCategory.parentId) return false;
|
||||
if (!draggingCategory.category) return false;
|
||||
if (target.classList.contains("task-planner-uncategorized-drop-zone")) return !draggingCategory.parentId;
|
||||
if (target.classList.contains("task-planner-category-boundary-drop-zone")) {
|
||||
const boundary = parseCategoryBoundaryDropId(target.dataset.dropId);
|
||||
return boundary.parentId === draggingCategory.parentId && boundary.category && boundary.category !== draggingCategory.category;
|
||||
}
|
||||
if (target.dataset.parentId !== draggingCategory.parentId) return false;
|
||||
if (target.classList.contains("task-planner-category-section")) return Boolean(target.dataset.category);
|
||||
return !target.dataset.category;
|
||||
},
|
||||
|
|
@ -351,6 +424,35 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
const fromCategory = parseCategorySectionId(fromId).category;
|
||||
const movingIds = getRootTaskIdsForCategory(data.tasks, parentMap, fromCategory);
|
||||
if (!movingIds.length) return;
|
||||
const rootCategories = getRootCategories(data.tasks, parentMap);
|
||||
if (isUncategorizedDropId(toId)) {
|
||||
const targetIds = data.tasks
|
||||
.filter((task) => !parentMap.get(task.id) && !getTaskCategory(task) && !movingIds.includes(task.id))
|
||||
.map((task) => task.id);
|
||||
const rootIds = getRootTaskIds(data.tasks, parentMap).filter((taskId) => !movingIds.includes(taskId));
|
||||
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, placement) || getBoundaryTaskId(data.tasks, rootIds, "before");
|
||||
if (boundaryTaskId) {
|
||||
save({
|
||||
...data,
|
||||
tasks: moveTaskGroup(data.tasks, movingIds, boundaryTaskId, "before"),
|
||||
categoryOrder: moveCategoryToStart(data.categoryOrder, rootCategories, fromCategory)
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isCategoryBoundaryDropId(toId)) {
|
||||
const boundary = parseCategoryBoundaryDropId(toId);
|
||||
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, boundary.category);
|
||||
const boundaryTaskId = getBoundaryTaskId(data.tasks, targetIds, boundary.placement);
|
||||
if (boundaryTaskId) {
|
||||
save({
|
||||
...data,
|
||||
tasks: moveTaskGroup(data.tasks, movingIds, boundaryTaskId, boundary.placement),
|
||||
categoryOrder: moveCategory(data.categoryOrder, rootCategories, fromCategory, boundary.category, boundary.placement)
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isCategorySectionId(toId)) {
|
||||
const toCategory = parseCategorySectionId(toId).category;
|
||||
const targetIds = getRootTaskIdsForCategory(data.tasks, parentMap, toCategory);
|
||||
|
|
@ -359,7 +461,7 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
save({
|
||||
...data,
|
||||
tasks: moveTaskGroup(data.tasks, movingIds, boundaryTaskId, placement),
|
||||
categoryOrder: moveCategory(data.categoryOrder, fromCategory, toCategory, placement)
|
||||
categoryOrder: moveCategory(data.categoryOrder, rootCategories, fromCategory, toCategory, placement)
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
|
@ -643,7 +745,15 @@ function TaskPlannerBranch({
|
|||
if (!children.length) return null;
|
||||
const nextVisited = new Set([...visited, ...children.map((task) => task.id)]);
|
||||
const draggingTask = draggingId ? getTaskById(tasks, draggingId) : null;
|
||||
const showUncategorizedDropZone = !parentId && draggingTask && !parentMap.get(draggingTask.id) && Boolean(getTaskCategoryDataset(draggingTask, tasks, parentMap));
|
||||
const draggingCategory = parseCategorySectionId(draggingCategoryId);
|
||||
const showUncategorizedDropZone = !parentId && (
|
||||
(draggingTask && !parentMap.get(draggingTask.id) && Boolean(getTaskCategoryDataset(draggingTask, tasks, parentMap)))
|
||||
|| Boolean(draggingCategory.category && !draggingCategory.parentId)
|
||||
);
|
||||
const showCategoryBoundaryDropZones = !parentId && (
|
||||
(draggingTask && !parentMap.get(draggingTask.id))
|
||||
|| Boolean(draggingCategory.category && !draggingCategory.parentId)
|
||||
);
|
||||
|
||||
function hasVisibleDescendants(taskId, seen = new Set()) {
|
||||
if (seen.has(taskId)) return false;
|
||||
|
|
@ -728,6 +838,8 @@ function TaskPlannerBranch({
|
|||
const groupCount = countTaskTree(groupTasks.map((task) => task.id), tasks, parentMap);
|
||||
const groupCompletedCount = countCompletedTaskTree(groupTasks.map((task) => task.id), tasks, parentMap);
|
||||
const categoryId = getCategorySectionId(parentId, group.category);
|
||||
const beforeDropId = getCategoryBoundaryDropId(parentId, group.category, "before");
|
||||
const afterDropId = getCategoryBoundaryDropId(parentId, group.category, "after");
|
||||
const isCollapsed = data.collapsedCategories.includes(group.category);
|
||||
const sectionClassName = [
|
||||
"task-planner-category-section",
|
||||
|
|
@ -737,7 +849,22 @@ function TaskPlannerBranch({
|
|||
categoryDropTarget.id === categoryId || dropTarget.id === categoryId ? "is-drop-target" : "",
|
||||
(categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after") || (dropTarget.id === categoryId && dropTarget.placement === "after") ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
return (
|
||||
const beforeDropZone = showCategoryBoundaryDropZones ? (
|
||||
<li
|
||||
className={`task-planner-category-boundary-drop-zone ${dropTarget.id === beforeDropId || categoryDropTarget.id === beforeDropId ? "is-drop-target" : ""}`}
|
||||
key={`${parentId || "root"}:${group.category}:before-drop`}
|
||||
data-drop-id={beforeDropId}
|
||||
/>
|
||||
) : null;
|
||||
const afterDropZone = showCategoryBoundaryDropZones ? (
|
||||
<li
|
||||
className={`task-planner-category-boundary-drop-zone ${dropTarget.id === afterDropId || categoryDropTarget.id === afterDropId ? "is-drop-target" : ""}`}
|
||||
key={`${parentId || "root"}:${group.category}:after-drop`}
|
||||
data-drop-id={afterDropId}
|
||||
/>
|
||||
) : null;
|
||||
return [
|
||||
beforeDropZone,
|
||||
<li className={sectionClassName} key={`${parentId || "root"}:${group.category}`} data-parent-id={parentId} data-category={group.category}>
|
||||
<div className="checklist-section-header task-planner-category-header">
|
||||
<div className="task-planner-category-title">
|
||||
|
|
@ -771,8 +898,9 @@ function TaskPlannerBranch({
|
|||
{groupTasks.map(renderTask).filter(Boolean)}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
</li>,
|
||||
afterDropZone
|
||||
];
|
||||
}
|
||||
|
||||
const entries = groupedTasks.entries.map((entry) => {
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ function normalizeComboSteps(inputs) {
|
|||
|
||||
function normalizeCombo(combo) {
|
||||
const name = String(combo?.name || "").trim().slice(0, 80);
|
||||
const category = String(combo?.category || "").trim().slice(0, 80);
|
||||
const device = COMBO_DEVICES.has(combo?.device) ? combo.device : "";
|
||||
const inputs = normalizeComboSteps(combo?.inputs);
|
||||
if (!name && !inputs.length) return null;
|
||||
|
|
@ -385,27 +386,32 @@ function normalizeCombo(combo) {
|
|||
name: name || "Combo",
|
||||
inputs
|
||||
};
|
||||
if (category) normalized.category = category;
|
||||
if (device) normalized.device = device;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeComboCategory(category) {
|
||||
const title = String(category?.title || "").trim().slice(0, 80);
|
||||
const combos = (Array.isArray(category?.combos) ? category.combos : []).map(normalizeCombo).filter(Boolean);
|
||||
if (!combos.length) return null;
|
||||
return {
|
||||
id: category?.id || uid("section"),
|
||||
title: title || "Catégorie",
|
||||
collapsed: category?.collapsed === true,
|
||||
combos
|
||||
};
|
||||
function normalizeComboCategoryOrder(value, combos) {
|
||||
const categories = combos.map((combo) => combo.category).filter(Boolean);
|
||||
const orderedCategories = (Array.isArray(value) ? value : [])
|
||||
.map((category) => String(category || "").trim().slice(0, 80))
|
||||
.filter((category, index, order) => category && categories.includes(category) && order.indexOf(category) === index);
|
||||
return [
|
||||
...orderedCategories,
|
||||
...categories.filter((category) => !orderedCategories.includes(category))
|
||||
];
|
||||
}
|
||||
|
||||
export function normalizeCombosData(data) {
|
||||
const combos = (Array.isArray(data?.combos) ? data.combos : []).map(normalizeCombo).filter(Boolean);
|
||||
const categoryOrder = normalizeComboCategoryOrder(data?.categoryOrder, combos);
|
||||
return {
|
||||
device: COMBO_DEVICES.has(data?.device) ? data.device : "playstation",
|
||||
combos: (Array.isArray(data?.combos) ? data.combos : []).map(normalizeCombo).filter(Boolean),
|
||||
categories: (Array.isArray(data?.categories) ? data.categories : []).map(normalizeComboCategory).filter(Boolean)
|
||||
combos,
|
||||
collapsedCategories: (Array.isArray(data?.collapsedCategories) ? data.collapsedCategories : [])
|
||||
.map((category) => String(category || "").trim().slice(0, 80))
|
||||
.filter((category, index, categories) => category && categoryOrder.includes(category) && categories.indexOf(category) === index),
|
||||
categoryOrder
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -759,31 +765,16 @@ export function compactModuleDataForStorage(type, value) {
|
|||
name: combo.name,
|
||||
inputs: combo.inputs
|
||||
};
|
||||
if (combo.category) compactCombo.category = combo.category;
|
||||
if (combo.device) compactCombo.device = combo.device;
|
||||
return compactCombo;
|
||||
}),
|
||||
categories: normalized.categories.map((category) => {
|
||||
const compactCategory = {
|
||||
id: category.id,
|
||||
title: category.title,
|
||||
combos: category.combos.map((combo) => {
|
||||
const compactCombo = {
|
||||
id: combo.id,
|
||||
name: combo.name,
|
||||
inputs: combo.inputs
|
||||
};
|
||||
if (combo.device) compactCombo.device = combo.device;
|
||||
return compactCombo;
|
||||
})
|
||||
};
|
||||
if (category.collapsed) compactCategory.collapsed = true;
|
||||
return compactCategory;
|
||||
})
|
||||
};
|
||||
if (normalized.device !== "playstation") compact.device = normalized.device;
|
||||
if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
|
||||
if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
|
||||
if (!compact.combos.length) delete compact.combos;
|
||||
if (!compact.categories.length) delete compact.categories;
|
||||
return compact.combos || compact.categories ? compact : null;
|
||||
return compact.combos ? compact : null;
|
||||
}
|
||||
if (type === "calculator") {
|
||||
const normalized = normalizeCalculatorData(value);
|
||||
|
|
@ -929,13 +920,8 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
const remapped = {};
|
||||
if (compact.device) remapped.device = compact.device;
|
||||
if (compact.combos) remapped.combos = compact.combos.map((combo) => ({ ...combo, id: nextId("combo") }));
|
||||
if (compact.categories) {
|
||||
remapped.categories = compact.categories.map((category) => ({
|
||||
...category,
|
||||
id: nextId("section"),
|
||||
combos: category.combos.map((combo) => ({ ...combo, id: nextId("combo") }))
|
||||
}));
|
||||
}
|
||||
if (compact.collapsedCategories) remapped.collapsedCategories = compact.collapsedCategories;
|
||||
if (compact.categoryOrder) remapped.categoryOrder = compact.categoryOrder;
|
||||
return remapped;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1882,6 +1882,19 @@ textarea:focus {
|
|||
box-shadow: inset 0 -3px 0 rgba(246, 196, 83, 0.8);
|
||||
}
|
||||
|
||||
.task-planner-category-boundary-drop-zone {
|
||||
min-height: 10px;
|
||||
border: 1px dashed rgba(165, 180, 252, 0.16);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.task-planner-category-boundary-drop-zone.is-drop-target {
|
||||
border-color: rgba(246, 196, 83, 0.72);
|
||||
background: rgba(246, 196, 83, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.24);
|
||||
}
|
||||
|
||||
.task-planner-category-section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
|
@ -2473,19 +2486,17 @@ textarea:focus {
|
|||
gap: 6px;
|
||||
}
|
||||
|
||||
.combos-root-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.combos-root-drop-zone {
|
||||
display: none;
|
||||
padding: 9px 12px;
|
||||
border: 1px dashed rgba(165, 180, 252, 0.22);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.2);
|
||||
background: rgba(15, 23, 42, 0.26);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.combos-root-drop-zone.is-visible,
|
||||
|
|
@ -2495,8 +2506,23 @@ textarea:focus {
|
|||
|
||||
.combos-root-drop-zone.is-drop-target {
|
||||
border-color: rgba(246, 196, 83, 0.72);
|
||||
background: rgba(246, 196, 83, 0.08);
|
||||
color: var(--color-text-primary);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.08), transparent 56%),
|
||||
rgba(15, 23, 42, 0.34);
|
||||
color: var(--color-accent-gold);
|
||||
box-shadow: inset 0 3px 0 rgba(246, 196, 83, 0.8);
|
||||
}
|
||||
|
||||
.combos-category-boundary-drop-zone {
|
||||
min-height: 6px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.combos-category-boundary-drop-zone.is-drop-target {
|
||||
min-height: 12px;
|
||||
border: 1px dashed rgba(246, 196, 83, 0.72);
|
||||
background: rgba(246, 196, 83, 0.12);
|
||||
box-shadow: inset 0 2px 0 rgba(246, 196, 83, 0.74);
|
||||
}
|
||||
|
||||
.combos-list {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue