Add notepad text import export
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
323e1b266d
commit
f76cddcc95
8 changed files with 165 additions and 19 deletions
|
|
@ -708,6 +708,7 @@ Règles communes :
|
||||||
|
|
||||||
Formats :
|
Formats :
|
||||||
|
|
||||||
|
- `notepad` : format structuré `@notepad`, bloc `@html` / `@endhtml` pour le HTML riche nettoyé, meta `@drawingMode: permanent` et `@drawings: {...}` pour les dessins permanents. Un collage texte simple reste accepté et devient une note sans mise en forme.
|
||||||
- `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`.
|
- `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`.
|
||||||
- `links` : lignes `Titre: https://...` ou URL seule.
|
- `links` : lignes `Titre: https://...` ou URL seule.
|
||||||
- `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées.
|
- `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées.
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,17 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.notepad.permanentStatus",
|
"toolboxes.modules.notepad.permanentStatus",
|
||||||
"toolboxes.modules.notepad.updatedAtPrefix",
|
"toolboxes.modules.notepad.updatedAtPrefix",
|
||||||
"toolboxes.modules.notepad.sessionStorageError",
|
"toolboxes.modules.notepad.sessionStorageError",
|
||||||
|
"toolboxes.modules.notepad.importPlaceholder",
|
||||||
|
"toolboxes.modules.notepad.importOpenButton",
|
||||||
|
"toolboxes.modules.notepad.exportOpenButton",
|
||||||
|
"toolboxes.modules.notepad.importModalTitle",
|
||||||
|
"toolboxes.modules.notepad.exportModalTitle",
|
||||||
|
"toolboxes.modules.notepad.importButton",
|
||||||
|
"toolboxes.modules.notepad.exportButton",
|
||||||
|
"toolboxes.modules.notepad.importSuccess",
|
||||||
|
"toolboxes.modules.notepad.exportSuccess",
|
||||||
|
"toolboxes.modules.notepad.importError",
|
||||||
|
"toolboxes.modules.notepad.exportInfo",
|
||||||
"toolboxes.modules.checklist.itemPlaceholder",
|
"toolboxes.modules.checklist.itemPlaceholder",
|
||||||
"toolboxes.modules.checklist.sectionPlaceholder",
|
"toolboxes.modules.checklist.sectionPlaceholder",
|
||||||
"toolboxes.modules.checklist.quantityLabel",
|
"toolboxes.modules.checklist.quantityLabel",
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(moduleRegistry, /Planificateur d'équipements/);
|
assert.match(moduleRegistry, /Planificateur d'équipements/);
|
||||||
assert.match(moduleRegistry, /imageAnnotation:/);
|
assert.match(moduleRegistry, /imageAnnotation:/);
|
||||||
assert.match(moduleRegistry, /Annotation d'images/);
|
assert.match(moduleRegistry, /Annotation d'images/);
|
||||||
assert.match(moduleRegistry, /editable: false/);
|
assert.match(moduleRegistry, /notepad: \{ label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: true \}/);
|
||||||
assert.match(moduleRegistry, /module-edit-button/);
|
assert.match(moduleRegistry, /module-edit-button/);
|
||||||
assert.match(moduleRegistry, /itemReorder\.startDrag/);
|
assert.match(moduleRegistry, /itemReorder\.startDrag/);
|
||||||
assert.match(moduleRegistry, /onPointerDown/);
|
assert.match(moduleRegistry, /onPointerDown/);
|
||||||
|
|
@ -159,6 +159,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(moduleRegistry, /CompactDropdown/);
|
assert.match(moduleRegistry, /CompactDropdown/);
|
||||||
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
||||||
assert.match(notepadModule, /export function NotepadModule/);
|
assert.match(notepadModule, /export function NotepadModule/);
|
||||||
|
assert.match(notepadModule, /TextExchangeActions/);
|
||||||
|
assert.match(notepadModule, /type="notepad"/);
|
||||||
assert.match(combosModule, /export function CombosModule/);
|
assert.match(combosModule, /export function CombosModule/);
|
||||||
assert.match(combosModule, /keyboardMouse/);
|
assert.match(combosModule, /keyboardMouse/);
|
||||||
assert.match(combosModule, /Nintendo 64/);
|
assert.match(combosModule, /Nintendo 64/);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ function createTextImportContext() {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
return {
|
return {
|
||||||
uid: (prefix) => `${prefix}-${index += 1}`,
|
uid: (prefix) => `${prefix}-${index += 1}`,
|
||||||
|
normalizeNotepadData,
|
||||||
normalizeChecklistData,
|
normalizeChecklistData,
|
||||||
normalizeLinksData,
|
normalizeLinksData,
|
||||||
normalizeCountersData,
|
normalizeCountersData,
|
||||||
|
|
@ -40,6 +41,23 @@ test("colon text import accepts labels without value", () => {
|
||||||
|
|
||||||
test("text exchange round trips simple toolbox modules and resets checklist progress", () => {
|
test("text exchange round trips simple toolbox modules and resets checklist progress", () => {
|
||||||
const context = createTextImportContext();
|
const context = createTextImportContext();
|
||||||
|
const notepad = normalizeNotepadData({
|
||||||
|
html: '<h3>Route</h3><p><strong style="color: #f6c453">Boss</strong></p>',
|
||||||
|
drawingMode: "permanent",
|
||||||
|
drawings: {
|
||||||
|
strokes: [
|
||||||
|
{ id: "old-stroke", color: "#22d3ee", width: 4, points: [{ x: 2, y: 3 }, { x: 8, y: 13 }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const notepadImport = importModuleText("notepad", exportModuleText("notepad", notepad), context);
|
||||||
|
assert.equal(notepadImport.ok, true);
|
||||||
|
assert.match(notepadImport.data.html, /<h3>Route<\/h3>/);
|
||||||
|
assert.match(notepadImport.data.html, /<strong style="color: #f6c453">Boss<\/strong>/);
|
||||||
|
assert.equal(notepadImport.data.drawingMode, "permanent");
|
||||||
|
assert.equal(notepadImport.data.drawings.strokes.length, 1);
|
||||||
|
assert.notEqual(notepadImport.data.drawings.strokes[0].id, "old-stroke");
|
||||||
|
|
||||||
const checklist = normalizeChecklistData({
|
const checklist = normalizeChecklistData({
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
|
|
@ -169,6 +187,8 @@ test("text exchange rejects invalid imports without data", () => {
|
||||||
assert.match(invalidImage.error, /^Ligne 1:/);
|
assert.match(invalidImage.error, /^Ligne 1:/);
|
||||||
assert.equal(invalidMarker.ok, false);
|
assert.equal(invalidMarker.ok, false);
|
||||||
assert.match(invalidMarker.error, /^Ligne 2:/);
|
assert.match(invalidMarker.error, /^Ligne 2:/);
|
||||||
|
assert.equal(importModuleText("notepad", "@notepad\n@html\n<p>Note</p>", context).ok, false);
|
||||||
|
assert.equal(importModuleText("notepad", "@notepad\n@drawings: {bad}", context).ok, false);
|
||||||
assert.equal(importModuleText("table", "\n", context).ok, false);
|
assert.equal(importModuleText("table", "\n", context).ok, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,8 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Le bloc notes conserve du texte riche et des dessins. Il n'utilise donc pas le format d'import/export texte des outils structurés.",
|
"text": "Le bloc notes utilise un bloc HTML nettoyé pour conserver la mise en forme autorisée, puis une ligne dessins en JSON compact.",
|
||||||
"example": "Le partage du bloc notes passe par l'export JSON de toolbox."
|
"example": "@notepad\n@html\n<h3>Obtenir Mew :</h3><div><ul><li>Obtenir la CS <span style=\"color: #f6c453\">Coupe </span>à l'intérieur du bateau le <span style=\"color: #d946ef\">S.S. Anne</span>.</li><li>Faire exprès de perdre un combat pour réapparaître au Centre Pokémon sans faire partir le navire.</li><li>Revenir sur les quais de <span style=\"color: #d946ef\">Carmin-sur-Mer</span> après avoir appris <span style=\"color: #f6c453\">Surf</span>.</li></ul></div>\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[...]}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
|
|
@ -300,7 +300,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Une ligne commençant par # crée une catégorie. Une ligne item peut contenir une quantité avec le format Nom:quantité.",
|
"text": "Une ligne commençant par # crée une catégorie. Une ligne item peut contenir une quantité avec le format Nom:quantité.",
|
||||||
"example": "# Potions\nPotion:10\nMéga potion:5\n\n# Armures\nCasque Rey Dau:1"
|
"example": "# Equipements\nEpee de diamant: 1\nBouclier: 1\nArc: 1\nCasque en diamant: 1\nArmure en diamant: 1\nGants en diamant: 1\nBottes en diamant: 1\n\nFleches: 64\nPotion de soin renforcée: 20\nPerle de l'Ender: 16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"counters": {
|
"counters": {
|
||||||
|
|
@ -363,7 +363,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Un compteur par ligne. Le libellé est séparé de la valeur avec deux-points.",
|
"text": "Un compteur par ligne. Le libellé est séparé de la valeur avec deux-points.",
|
||||||
"example": "Victoires: 12\nDéfaites: -2\nEssais boss: 34"
|
"example": "Top 1: 3\nTop 2: 6\nTop 3: 2\nTop 4: 1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"images": {
|
"images": {
|
||||||
|
|
@ -428,7 +428,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Chaque image est exportée comme un bloc avec un libellé optionnel puis une data URL base64 complète.",
|
"text": "Chaque image est exportée comme un bloc avec un libellé optionnel puis une data URL base64 complète.",
|
||||||
"example": "# Map zone nord\ndata:image/webp;base64,UklGRiQAAABXRUJQVlA4...\n\n# Capture boss\ndata:image/png;base64,iVBORw0KGgoAAAANS..."
|
"example": "# Heal composition strategy\ndata:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABA...\n# Healers counters\ndata:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABA..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"imageAnnotation": {
|
"imageAnnotation": {
|
||||||
|
|
@ -548,7 +548,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Le format contient l'image en base64, puis les marqueurs et les dessins permanents.",
|
"text": "Le format contient l'image en base64, puis les marqueurs et les dessins permanents.",
|
||||||
"example": "data:image/webp;base64,UklGRiQAAABXRUJQVlA4...\n@marker: 42.5,68,Entrée\n@drawings: {\"strokes\":[]}"
|
"example": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABA...\n@marker: 63.85,49.46,Niv. 1-20\n@marker: 47.65,31.93,Niv. 10-20\n@marker: 55.92,41.03,Niv. 20-30\n@drawings: {\"strokes\":[...]}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
|
|
@ -595,7 +595,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Un lien par ligne. Séparez le titre et l'URL avec deux-points.",
|
"text": "Un lien par ligne. Séparez le titre et l'URL avec deux-points.",
|
||||||
"example": "Build:https://example.com/build?class=rogue\nMap:https://example.com/map"
|
"example": "Sokko G: https://sokkog.shinuwa.fr/#/\nC'est quoi ?: https://sokkog.shinuwa.fr/#/about"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calculator": {
|
"calculator": {
|
||||||
|
|
@ -670,7 +670,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Une ligne par résultat. L'indentation de deux espaces conserve les liens parent/enfant.",
|
"text": "Une ligne par résultat. L'indentation de deux espaces conserve les liens parent/enfant.",
|
||||||
"example": "@scrollResults: true\nLingots: 100\n Minerai: 500\nCristaux: 12"
|
"example": "Lingot de champignite: 100\n Lingot de chlorophyte: 100\n Champignon luisant: 1500\nLingot de chlorophyte: 100\n Minerai de chlorophyte: 500"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|
@ -750,7 +750,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Un TSV pur peut être importé. L'export TSV du panneau d'ajout ajoute des lignes @ pour préserver dimensions, intitulés et formules.",
|
"text": "Un TSV pur peut être importé. L'export TSV du panneau d'ajout ajoute des lignes @ pour préserver dimensions, intitulés et formules.",
|
||||||
"example": "@size: 2x3\n@columns: Objet\tValeur\tTotal\n@tsv\nOeuf d'or\t20000\t=B1*3\nOeuf d'argent\t5000\t=B2*2"
|
"example": "@size: 4x4\n@columns: Objet\tValeur\tQuantité\tValeur\n@tsv\nOeuf d'acier\t1000\t563\t=B1*C1\nOeuf d'argent\t5000\t153\t=B2*C2\nOeuf d'or\t20000\t69\t=B3*C3\n\t\t\t=(D1+D2+D3)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timer": {
|
"timer": {
|
||||||
|
|
@ -881,7 +881,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Les étapes et comptes à rebours sont exportés en lignes lisibles. Les échéances sont recalculées à l'import.",
|
"text": "Les étapes et comptes à rebours sont exportés en lignes lisibles. Les échéances sont recalculées à l'import.",
|
||||||
"example": "@activeTab: countdown\nlap | label=Phase 1 | elapsedMs=90000\ncountdown | label=Boss | type=duration | durationMs=300000\ncountdown | label=Event | type=time_pattern | pattern=X:45:00"
|
"example": "@activeTab: countdown\nlap | label=Fosse 110 | elapsedMs=1046475\nlap | label=Fosse 55 | elapsedMs=50019\ncountdown | label=Fin de vague infernale | type=time_pattern | alertMode=off | pattern=X:55:X\ncountdown | label=Boss Mondial | type=daily_time | alertMode=off | time=14:00:00\ncountdown | label=Legion | type=interval | alertMode=off | autoRefresh=true | intervalMs=1800000 | startMode=now\ncountdown | label=Durée | type=duration | alertMode=off | durationMs=1800000"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"taskPlanner": {
|
"taskPlanner": {
|
||||||
|
|
@ -964,7 +964,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Les catégories commencent par #. Les tâches utilisent leur type et l'indentation de deux espaces crée les sous-tâches. Les états cochés ne sont pas repris.",
|
"text": "Les catégories commencent par #. Les tâches utilisent leur type et l'indentation de deux espaces crée les sous-tâches. Les états cochés ne sont pas repris.",
|
||||||
"example": "@resetTime: 06:00\n# Raid\n- daily Préparer la route\n @description: Prendre consommables\n - unique Acheter potions\n @prerequisite: true\n- weekly Coffre hebdo"
|
"example": "@hideCompleted: true\n# To craft\n- unique Essence de liqueur\n- unique Bière\n- unique Vinaigre\n- weekly Faire les boss du Sanctuaire Noir (Groupe)\n @weeklyResetDay: 0\n- weekly Faire les boss du Sanctuaire Noir (Solo)\n @weeklyResetDay: 0\n- unique Ameliorer le bateau\n - daily Faire du troc\n @prerequisite: true"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"combos": {
|
"combos": {
|
||||||
|
|
@ -1081,7 +1081,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Les catégories commencent par #. Un combo décrit son périphérique et ses étapes avec >, tandis que + groupe des inputs simultanés.",
|
"text": "Les catégories commencent par #. Un combo décrit son périphérique et ses étapes avec >, tandis que + groupe des inputs simultanés.",
|
||||||
"example": "@device: playstation\n# Neutral\nHadoken | device=playstation | direction:down > direction:right > button:cross\nCharge | button:square[2s] > button:triangle+button:circle"
|
"example": "# Ryu\nHadoken | device=playstation | direction:down > direction:down-right > direction:right+button:square\nShoryken | device=playstation | direction:right > direction:down > direction:down-right > direction:right+button:fist\n# Chun Li\nKikoken | device=playstation | direction:left[hold] > direction:right+button:fist\nHundred Lightning Kicks | device=playstation | direction:down > direction:down-right > direction:right+button:foot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"equipmentPlanner": {
|
"equipmentPlanner": {
|
||||||
|
|
@ -1146,7 +1146,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Le format est découpé par type, équipement et sous-sections Stats, Sockets et Craft.",
|
"text": "Le format est découpé par type, équipement et sous-sections Stats, Sockets et Craft.",
|
||||||
"example": "# Armes | icon=sword\n## Épée runique | icon=sword | active=true\nStats\nDégâts | sword | Attaque | 12\nSockets\nJoyau attaque | shape=jewel | color=yellow\n bonus | Compétence | sword | Attaque | 1\nCraft\nMinerai rare: 4"
|
"example": "# Casque | icon=helmet\n## Nu Udra γ | icon=helmet | active=true\nStats\n | shield | Défense | 68\n | shield | Resistance au feu | 5\n | shield | Resistance a l'eau | -4\nSockets\nJoyau antivirus | shape=jewel | color=violet\n bonus | | sword | Antivirus | 1\nCraft\n@obtain: Avoir un certificat Alpha supreme\n## Nu Udra γ | icon=chest-armor | active=true\nStats\n | shield | Défense | 68\nSockets\nJoyau Attaque | shape=jewel | color=red\n bonus | | sword | Attaque | 2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1467,7 +1467,18 @@
|
||||||
"temporaryStatus": "Dessins temporaires",
|
"temporaryStatus": "Dessins temporaires",
|
||||||
"permanentStatus": "Dessins permanents",
|
"permanentStatus": "Dessins permanents",
|
||||||
"updatedAtPrefix": "Modifié le",
|
"updatedAtPrefix": "Modifié le",
|
||||||
"sessionStorageError": "Les annotations temporaires n'ont pas pu être sauvegardées."
|
"sessionStorageError": "Les annotations temporaires n'ont pas pu être sauvegardées.",
|
||||||
|
"importPlaceholder": "@notepad\n@html\n<h3>Route boss</h3>\n<p><strong style=\"color: #f6c453\">Préparer les consommables</strong></p>\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[]}",
|
||||||
|
"importOpenButton": "Importer du texte",
|
||||||
|
"exportOpenButton": "Exporter en format texte",
|
||||||
|
"importModalTitle": "Importer un bloc notes",
|
||||||
|
"exportModalTitle": "Exporter un bloc notes",
|
||||||
|
"importButton": "Importer le texte",
|
||||||
|
"exportButton": "Copier le texte",
|
||||||
|
"importSuccess": "Import bloc notes terminé.",
|
||||||
|
"exportSuccess": "Export bloc notes copié.",
|
||||||
|
"importError": "Format de bloc notes invalide.",
|
||||||
|
"exportInfo": "Le HTML est nettoyé à l'import. Les dessins permanents sont exportés en JSON compact."
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"itemPlaceholder": "Nouvel item",
|
"itemPlaceholder": "Nouvel item",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||||
import { DrawingControls, FormattingColorDropdown } from "./DrawingControls.jsx";
|
import { DrawingControls, FormattingColorDropdown } from "./DrawingControls.jsx";
|
||||||
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
||||||
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
|
|
||||||
const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"];
|
const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"];
|
||||||
const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"];
|
const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"];
|
||||||
|
|
@ -107,7 +108,7 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotepadModule({ toolboxId, moduleId, context }) {
|
export function NotepadModule({ toolboxId, moduleId, context, editing = false }) {
|
||||||
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
|
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
|
||||||
const normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]);
|
const normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]);
|
||||||
const textContent = context.moduleText?.notepad || {};
|
const textContent = context.moduleText?.notepad || {};
|
||||||
|
|
@ -142,7 +143,7 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
setDrawingMode(normalizedData.drawingMode);
|
setDrawingMode(normalizedData.drawingMode);
|
||||||
setDrawingActive(false);
|
setDrawingActive(false);
|
||||||
setEraseActive(false);
|
setEraseActive(false);
|
||||||
}, [toolboxId, moduleId]);
|
}, [toolboxId, moduleId, normalizedData.html, normalizedData.text, normalizedData.updatedAt, normalizedData.drawingMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorRef.current || editorRef.current.innerHTML === html) return;
|
if (!editorRef.current || editorRef.current.innerHTML === html) return;
|
||||||
|
|
@ -302,6 +303,11 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="notepad-module">
|
<div className="notepad-module">
|
||||||
|
{editing && (
|
||||||
|
<div className="module-add-panel notepad-exchange-panel">
|
||||||
|
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="notepad-toolbar" aria-label={textContent.toolbarLabel || "Mise en page du bloc notes"}>
|
<div className="notepad-toolbar" aria-label={textContent.toolbarLabel || "Mise en page du bloc notes"}>
|
||||||
<div className="notepad-format-controls">
|
<div className="notepad-format-controls">
|
||||||
<div className="notepad-toolbar-group" role="group" aria-label={textContent.textFormatLabel || "Style du texte"}>
|
<div className="notepad-toolbar-group" role="group" aria-label={textContent.textFormatLabel || "Style du texte"}>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
|
||||||
import { TimerModule } from "./TimerModule.jsx";
|
import { TimerModule } from "./TimerModule.jsx";
|
||||||
|
|
||||||
const MODULE_COMPONENTS = {
|
const MODULE_COMPONENTS = {
|
||||||
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
|
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: true },
|
||||||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true },
|
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true },
|
||||||
images: { label: "Images", icon: "picture", Component: ImagesModule, editable: true, scrollable: true },
|
images: { label: "Images", icon: "picture", Component: ImagesModule, editable: true, scrollable: true },
|
||||||
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true, scrollable: true },
|
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true, scrollable: true },
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export function parseColonImportLines(text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
|
const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
|
||||||
|
"notepad",
|
||||||
"checklist",
|
"checklist",
|
||||||
"links",
|
"links",
|
||||||
"counters",
|
"counters",
|
||||||
|
|
@ -42,6 +43,7 @@ export function supportsTextExchange(type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasTextExchangeContent(type, data) {
|
export function hasTextExchangeContent(type, data) {
|
||||||
|
if (type === "notepad") return Boolean(data?.text || data?.html || data?.drawings?.strokes?.length);
|
||||||
if (type === "checklist") return Boolean(data?.sections?.some((section) => section.items?.length));
|
if (type === "checklist") return Boolean(data?.sections?.some((section) => section.items?.length));
|
||||||
if (type === "links") return Boolean(data?.links?.length);
|
if (type === "links") return Boolean(data?.links?.length);
|
||||||
if (type === "counters") return Boolean(data?.counters?.length);
|
if (type === "counters") return Boolean(data?.counters?.length);
|
||||||
|
|
@ -57,6 +59,7 @@ export function hasTextExchangeContent(type, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportModuleText(type, data) {
|
export function exportModuleText(type, data) {
|
||||||
|
if (type === "notepad") return exportNotepadText(data);
|
||||||
if (type === "checklist") return exportChecklistText(data);
|
if (type === "checklist") return exportChecklistText(data);
|
||||||
if (type === "links") return exportLinksText(data);
|
if (type === "links") return exportLinksText(data);
|
||||||
if (type === "counters") return exportCountersText(data);
|
if (type === "counters") return exportCountersText(data);
|
||||||
|
|
@ -76,7 +79,8 @@ export function importModuleText(type, text, context) {
|
||||||
const value = String(text || "");
|
const value = String(text || "");
|
||||||
if (!value.trim()) return fail("Le texte à importer est vide.");
|
if (!value.trim()) return fail("Le texte à importer est vide.");
|
||||||
let data = null;
|
let data = null;
|
||||||
if (type === "checklist") data = importChecklistText(value, context);
|
if (type === "notepad") data = importNotepadText(value, context);
|
||||||
|
else if (type === "checklist") data = importChecklistText(value, context);
|
||||||
else if (type === "links") data = importLinksText(value, context);
|
else if (type === "links") data = importLinksText(value, context);
|
||||||
else if (type === "counters") data = importCountersText(value, context);
|
else if (type === "counters") data = importCountersText(value, context);
|
||||||
else if (type === "combos") data = importCombosText(value, context);
|
else if (type === "combos") data = importCombosText(value, context);
|
||||||
|
|
@ -157,6 +161,97 @@ function cloneDrawingData(drawings) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
function textToNotepadExchangeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>")
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportNotepadText(data) {
|
||||||
|
const html = String(data?.html || "").trim() || textToNotepadExchangeHtml(data?.text || "");
|
||||||
|
const lines = ["@notepad", "@html", html, "@endhtml"];
|
||||||
|
if (data?.drawingMode === "permanent") lines.push("@drawingMode: permanent");
|
||||||
|
if (data?.drawings?.strokes?.length) lines.push(`@drawings: ${JSON.stringify(cloneDrawingData(data.drawings))}`);
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneImportedNotepadDrawings(drawings, context) {
|
||||||
|
if (!drawings || typeof drawings !== "object" || !Array.isArray(drawings.strokes)) throw new Error("dessins invalides.");
|
||||||
|
return {
|
||||||
|
strokes: drawings.strokes.map((stroke) => {
|
||||||
|
if (!stroke || typeof stroke !== "object" || !Array.isArray(stroke.points) || stroke.points.length < 2) throw new Error("trait de dessin invalide.");
|
||||||
|
return {
|
||||||
|
...stroke,
|
||||||
|
id: context.uid("stroke"),
|
||||||
|
points: stroke.points.map((point) => {
|
||||||
|
const x = Number(point?.x);
|
||||||
|
const y = Number(point?.y);
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("point de dessin invalide.");
|
||||||
|
return { x, y };
|
||||||
|
})
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function importNotepadText(text, context) {
|
||||||
|
const lines = splitLines(text);
|
||||||
|
const hasStructuredMarker = lines.some((line) => ["@notepad", "@html", "@endhtml"].includes(line.trim()) || parseMetaLine(line));
|
||||||
|
if (!hasStructuredMarker) return context.normalizeNotepadData({ text });
|
||||||
|
|
||||||
|
let readingHtml = false;
|
||||||
|
let htmlWasOpened = false;
|
||||||
|
let htmlWasClosed = false;
|
||||||
|
const htmlLines = [];
|
||||||
|
let drawingMode = "temporary";
|
||||||
|
let drawings = { strokes: [] };
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (readingHtml) {
|
||||||
|
if (cleanLine === "@endhtml") {
|
||||||
|
readingHtml = false;
|
||||||
|
htmlWasClosed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
htmlLines.push(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cleanLine || cleanLine === "@notepad") return;
|
||||||
|
if (cleanLine === "@html") {
|
||||||
|
if (htmlWasOpened) throw lineError(index, "bloc html dupliqué.");
|
||||||
|
readingHtml = true;
|
||||||
|
htmlWasOpened = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta?.key === "drawingMode") {
|
||||||
|
if (!["temporary", "permanent"].includes(meta.value)) throw lineError(index, "mode de dessin invalide.");
|
||||||
|
drawingMode = meta.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (meta?.key === "drawings") {
|
||||||
|
try {
|
||||||
|
drawings = cloneImportedNotepadDrawings(JSON.parse(meta.value), context);
|
||||||
|
} catch (error) {
|
||||||
|
throw lineError(index, error?.message || "dessins invalides.");
|
||||||
|
}
|
||||||
|
if (drawings.strokes.length) drawingMode = "permanent";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw lineError(index, `ligne bloc notes invalide: ${cleanLine}`);
|
||||||
|
});
|
||||||
|
if (readingHtml || (htmlWasOpened && !htmlWasClosed)) throw new Error("Bloc HTML non terminé.");
|
||||||
|
return context.normalizeNotepadData({ html: htmlLines.join("\n").trim(), drawingMode, drawings });
|
||||||
|
}
|
||||||
|
|
||||||
function exportChecklistText(data) {
|
function exportChecklistText(data) {
|
||||||
return (data.sections || []).flatMap((section) => [
|
return (data.sections || []).flatMap((section) => [
|
||||||
section.title ? `# ${section.title}` : "",
|
section.title ? `# ${section.title}` : "",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue