143 lines
5.6 KiB
JavaScript
143 lines
5.6 KiB
JavaScript
// Rôle : fournit l'outil liens avec ajout manuel et import texte.
|
|
import { useCallback, useState } from "react";
|
|
import { Icon } from "../../../components/Icon.jsx";
|
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
|
import { TextImportModal } from "./TextImportModal.jsx";
|
|
import { parseColonImportLines } from "./textImport.js";
|
|
|
|
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
|
const textContent = context.moduleText?.links || {};
|
|
const linkDraft = useDraftForm({ title: "", url: "" });
|
|
const [textImport, setTextImport] = useState("");
|
|
const [importOpen, setImportOpen] = useState(false);
|
|
const [copiedId, setCopiedId] = useState("");
|
|
const closeImportModal = useCallback(() => setImportOpen(false), []);
|
|
const {
|
|
itemReorder,
|
|
getItemProps,
|
|
isItemDragging,
|
|
isItemDropTarget,
|
|
getDropPlacement
|
|
} = useGroupedReorder({
|
|
namespace: "links",
|
|
items: data.links,
|
|
getItemId: (link) => link.id,
|
|
getParentId: () => moduleId,
|
|
onItemMove: (operation) => save(moveItem(data.links, operation.sourceId, operation.targetId, operation.placement)),
|
|
hierarchy: { enabled: true, stickyParents: true }
|
|
});
|
|
|
|
function save(links) {
|
|
context.setModuleData(toolboxId, moduleId, { links });
|
|
}
|
|
|
|
function addLink(event) {
|
|
linkDraft.handleSubmit(event, (draft, { reset }) => {
|
|
const cleanUrl = context.normalizeUrl(draft.url);
|
|
if (!cleanUrl) return;
|
|
|
|
save([
|
|
...data.links,
|
|
{
|
|
id: context.uid("link"),
|
|
title: draft.title.trim(),
|
|
url: cleanUrl
|
|
}
|
|
]);
|
|
reset();
|
|
});
|
|
}
|
|
|
|
function importLinks(event) {
|
|
event.preventDefault();
|
|
const imported = parseColonImportLines(textImport)
|
|
.map((entry) => ({
|
|
id: context.uid("link"),
|
|
title: entry.label,
|
|
url: context.normalizeUrl(entry.value)
|
|
}))
|
|
.filter((link) => link.url);
|
|
if (!imported.length) return;
|
|
save([...data.links, ...imported]);
|
|
setTextImport("");
|
|
setImportOpen(false);
|
|
context.notify?.("Import de liens terminé.");
|
|
}
|
|
|
|
async function copyUrl(link) {
|
|
const copied = await context.copyText(link.url);
|
|
if (!copied) return;
|
|
setCopiedId(link.id);
|
|
context.notify?.(textContent.copiedTitle || "Copié");
|
|
window.setTimeout(() => setCopiedId(""), 1400);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{editing && (
|
|
<div className="module-add-panel">
|
|
<form className="inline-form links-add-form" onSubmit={addLink}>
|
|
<input {...linkDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nom du lien" })} />
|
|
<input {...linkDraft.getFieldProps("url", { placeholder: textContent.urlPlaceholder || "https://..." })} />
|
|
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
|
</form>
|
|
<div className="text-import-actions">
|
|
<button type="button" onClick={() => setImportOpen(true)}>
|
|
<Icon name="import" />
|
|
<span>{textContent.importOpenButton || "Importer du texte"}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{importOpen && (
|
|
<TextImportModal
|
|
title={textContent.importModalTitle || "Importer des liens"}
|
|
value={textImport}
|
|
onChange={setTextImport}
|
|
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
|
|
submitLabel={textContent.importButton || "Importer le texte"}
|
|
onSubmit={importLinks}
|
|
onClose={closeImportModal}
|
|
/>
|
|
)}
|
|
<ul className="links-list">
|
|
{data.links.map((link) => {
|
|
const className = [
|
|
"link-item",
|
|
isItemDragging(link.id) ? "is-dragging" : "",
|
|
isItemDropTarget(link.id) ? "is-drop-target" : "",
|
|
getDropPlacement("item", link.id) === "after" ? "drop-after" : ""
|
|
].filter(Boolean).join(" ");
|
|
|
|
return (
|
|
<li className={className} key={link.id} {...getItemProps({ itemId: link.id, parentId: moduleId })}>
|
|
<button
|
|
className="link-drag-handle"
|
|
type="button"
|
|
onPointerDown={(event) => itemReorder.startDrag(event, link.id)}
|
|
aria-label={`${textContent.reorderTitle || "Déplacer"} ${link.title || link.url}`}
|
|
title={textContent.reorderTitle || "Déplacer"}
|
|
>
|
|
<Icon name="drag" />
|
|
</button>
|
|
<a href={link.url} target="_blank" rel="noreferrer" title={link.url}>
|
|
<strong>{link.title || context.hostnameFromUrl(link.url)}</strong>
|
|
<span>{link.url}</span>
|
|
</a>
|
|
<div>
|
|
<button type="button" onClick={() => copyUrl(link)} aria-label={`${textContent.copyTitle || "Copier"} ${link.title || link.url}`} title={copiedId === link.id ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier"}>
|
|
<Icon name="copy" />
|
|
</button>
|
|
<button type="button" className="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${link.title || link.url}`} title={textContent.deleteTitle || "Supprimer"}>
|
|
<Icon name="trash" />
|
|
</button>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</>
|
|
);
|
|
}
|