220 lines
8.6 KiB
JavaScript
220 lines
8.6 KiB
JavaScript
// Rôle : fournit l'outil de mémos courts avec post-it, bande déroulante et tri manuel.
|
|
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
|
import { Icon } from "../../../components/Icon.jsx";
|
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
|
import { TEXT_COLORS } from "./notepadUtils.js";
|
|
|
|
export function MemosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
|
const storedData = context.getModuleData(toolboxId, moduleId, { items: [] });
|
|
const data = useMemo(() => context.normalizeMemosData(storedData), [context, storedData]);
|
|
const textContent = context.moduleText?.memos || {};
|
|
const tickerItems = data.tickerEnabled ? data.items : [];
|
|
const textareaRefs = useRef(new Map());
|
|
const [drafts, setDrafts] = useState({});
|
|
const {
|
|
itemReorder,
|
|
getItemProps,
|
|
isItemDragging,
|
|
isItemDropTarget,
|
|
getDropPlacement
|
|
} = useGroupedReorder({
|
|
namespace: `memos-${moduleId}`,
|
|
items: data.items,
|
|
getItemId: (memo) => memo.id,
|
|
getParentId: () => moduleId,
|
|
orientation: "horizontal",
|
|
onItemMove: (operation) => saveItems(moveItem(data.items, operation.sourceId, operation.targetId, operation.placement)),
|
|
hierarchy: { enabled: true, stickyParents: true }
|
|
});
|
|
|
|
useEffect(() => {
|
|
setDrafts(Object.fromEntries(data.items.map((item) => [item.id, item.text])));
|
|
}, [data.items]);
|
|
|
|
useLayoutEffect(() => {
|
|
data.items.forEach((item) => resizeMemoTextarea(textareaRefs.current.get(item.id)));
|
|
}, [data.items, drafts]);
|
|
|
|
function resizeMemoTextarea(element) {
|
|
if (!element) return;
|
|
element.style.height = "auto";
|
|
element.style.height = `${element.scrollHeight + 2}px`;
|
|
}
|
|
|
|
function save(nextData) {
|
|
context.setModuleData(toolboxId, moduleId, nextData, "memos");
|
|
}
|
|
|
|
function saveItems(items) {
|
|
save({ ...data, items });
|
|
}
|
|
|
|
function addMemo() {
|
|
const now = new Date().toISOString();
|
|
save({
|
|
...data,
|
|
items: [
|
|
{ id: context.uid("memo"), text: textContent.defaultText || "Nouveau mémo", color: "#f6c453", updatedAt: now },
|
|
...data.items
|
|
]
|
|
});
|
|
}
|
|
|
|
function updateMemo(memoId, updater) {
|
|
const now = new Date().toISOString();
|
|
save({
|
|
...data,
|
|
items: data.items.map((item) => item.id === memoId ? { ...updater(item), updatedAt: now } : item)
|
|
});
|
|
}
|
|
|
|
function deleteMemo(memoId) {
|
|
save({ ...data, items: data.items.filter((item) => item.id !== memoId) });
|
|
}
|
|
|
|
function commitMemoText(memoId) {
|
|
const nextText = String(drafts[memoId] || "").trim();
|
|
if (!nextText) {
|
|
deleteMemo(memoId);
|
|
return;
|
|
}
|
|
updateMemo(memoId, (item) => ({ ...item, text: nextText }));
|
|
}
|
|
|
|
return (
|
|
<div className="memos-module">
|
|
{textExchangeOpen && (
|
|
<div className="module-text-exchange-panel">
|
|
<TextExchangeActions type="memos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
|
</div>
|
|
)}
|
|
|
|
{editing && (
|
|
<div className="module-add-panel">
|
|
<button className="memos-add-button nds-button" data-size="sm" data-variant="primary" type="button" onClick={addMemo}>
|
|
<Icon name="add" />
|
|
<span>{textContent.addButton || "Ajouter"}</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="memos-toolbar">
|
|
<button
|
|
className={`memos-ticker-toggle nds-icon-button ${data.tickerEnabled ? "active" : ""}`}
|
|
data-size="sm"
|
|
type="button"
|
|
onClick={() => save({ ...data, tickerEnabled: !data.tickerEnabled })}
|
|
aria-pressed={data.tickerEnabled}
|
|
title={data.tickerEnabled ? textContent.hideTickerTitle || "Masquer la bande" : textContent.showTickerTitle || "Afficher la bande"}
|
|
>
|
|
<Icon name={data.tickerEnabled ? "eye-open" : "eye-closed"} />
|
|
</button>
|
|
{tickerItems.length > 0 && (
|
|
<div className="memos-ticker" aria-label={textContent.tickerLabel || "Mémos"}>
|
|
<div className="memos-ticker-track">
|
|
{[...tickerItems, ...tickerItems].map((memo, index) => (
|
|
<span className="memos-ticker-item" key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
|
|
{memo.text}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="memos-list">
|
|
{data.items.length ? data.items.map((memo) => {
|
|
const className = [
|
|
"memo-card",
|
|
isItemDragging(memo.id) ? "is-dragging" : "",
|
|
isItemDropTarget(memo.id) ? "is-drop-target" : "",
|
|
getDropPlacement("item", memo.id) === "after" ? "drop-after" : ""
|
|
].filter(Boolean).join(" ");
|
|
|
|
return (
|
|
<article className={className} key={memo.id} style={{ "--memo-color": memo.color }} {...getItemProps({ itemId: memo.id, parentId: moduleId })}>
|
|
<button
|
|
className="memo-drag-handle nds-icon-button"
|
|
data-size="sm"
|
|
data-variant="ghost"
|
|
type="button"
|
|
onPointerDown={(event) => itemReorder.startDrag(event, memo.id)}
|
|
aria-label={`${textContent.reorderTitle || "Déplacer"} ${memo.text}`}
|
|
title={textContent.reorderTitle || "Déplacer"}
|
|
>
|
|
<Icon name="drag" />
|
|
</button>
|
|
<textarea
|
|
className="nds-textarea"
|
|
data-size="sm"
|
|
data-variant="ghost"
|
|
ref={(element) => {
|
|
if (element) {
|
|
textareaRefs.current.set(memo.id, element);
|
|
resizeMemoTextarea(element);
|
|
} else {
|
|
textareaRefs.current.delete(memo.id);
|
|
}
|
|
}}
|
|
value={drafts[memo.id] ?? memo.text}
|
|
rows={3}
|
|
placeholder={textContent.placeholder || "Mémo court..."}
|
|
onChange={(event) => {
|
|
resizeMemoTextarea(event.currentTarget);
|
|
setDrafts((current) => ({ ...current, [memo.id]: event.target.value }));
|
|
}}
|
|
onBlur={() => commitMemoText(memo.id)}
|
|
aria-label={textContent.editLabel || "Modifier le mémo"}
|
|
/>
|
|
<footer>
|
|
<CompactDropdown
|
|
className="memo-color-dropdown notepad-color-dropdown"
|
|
menuClassName="memo-color-menu notepad-color-menu"
|
|
label={textContent.colorLabel || "Couleur du mémo"}
|
|
renderTrigger={({ toggle }) => (
|
|
<button
|
|
className="notepad-color-toggle nds-swatch"
|
|
type="button"
|
|
style={{ "--swatch-color": memo.color }}
|
|
onClick={toggle}
|
|
title={textContent.colorLabel || "Couleur du mémo"}
|
|
aria-label={textContent.colorLabel || "Couleur du mémo"}
|
|
/>
|
|
)}
|
|
>
|
|
{({ close }) => (
|
|
<>
|
|
{TEXT_COLORS.map((color) => (
|
|
<button
|
|
key={color}
|
|
className={`notepad-swatch nds-swatch ${memo.color === color ? "active" : ""}`}
|
|
type="button"
|
|
style={{ "--swatch-color": color }}
|
|
onClick={() => {
|
|
updateMemo(memo.id, (item) => ({ ...item, color }));
|
|
close();
|
|
}}
|
|
aria-label={`${textContent.colorLabel || "Couleur"} ${color}`}
|
|
/>
|
|
))}
|
|
</>
|
|
)}
|
|
</CompactDropdown>
|
|
<button className="memo-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => deleteMemo(memo.id)} aria-label={textContent.deleteTitle || "Supprimer le mémo"} title={textContent.deleteTitle || "Supprimer"}>
|
|
<Icon name="trash" />
|
|
</button>
|
|
</footer>
|
|
</article>
|
|
);
|
|
}) : (
|
|
<button className="memos-empty nds-button" data-size="sm" data-variant="dashed" type="button" onClick={addMemo}>
|
|
<Icon name="add" />
|
|
<span>{textContent.emptyLabel || "Ajouter un premier mémo"}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|