improve text editor toolbar
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
de1b8a5638
commit
896ae23ff2
6 changed files with 148 additions and 11 deletions
|
|
@ -186,7 +186,9 @@ Le bloc notes utilise une edition directe en `contentEditable`, sans mode previe
|
||||||
Regles UI :
|
Regles UI :
|
||||||
|
|
||||||
- toolbar compacte en haut de l'outil ;
|
- toolbar compacte en haut de l'outil ;
|
||||||
- boutons courts pour gras, italique, souligne, paragraphes, titres et listes ;
|
- boutons courts pour gras, italique, souligne, barre, titre et listes ;
|
||||||
|
- le paragraphe est le style par defaut et n'a pas de bouton dedie ;
|
||||||
|
- les types de listes sont regroupes dans un dropdown compact ;
|
||||||
- couleurs texte et surlignage regroupees dans un dropdown unique avec icone arc-en-ciel et libelles de categories ;
|
- couleurs texte et surlignage regroupees dans un dropdown unique avec icone arc-en-ciel et libelles de categories ;
|
||||||
- date de derniere modification affichee en pied d'outil, sans prendre le dessus sur le contenu ;
|
- date de derniere modification affichee en pied d'outil, sans prendre le dessus sur le contenu ;
|
||||||
- switch de stockage des dessins en pied d'outil, avec icone sauvegarde et etat actif ;
|
- switch de stockage des dessins en pied d'outil, avec icone sauvegarde et etat actif ;
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,9 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.notepad.boldTitle",
|
"toolboxes.modules.notepad.boldTitle",
|
||||||
"toolboxes.modules.notepad.italicTitle",
|
"toolboxes.modules.notepad.italicTitle",
|
||||||
"toolboxes.modules.notepad.underlineTitle",
|
"toolboxes.modules.notepad.underlineTitle",
|
||||||
"toolboxes.modules.notepad.paragraphTitle",
|
"toolboxes.modules.notepad.strikeTitle",
|
||||||
"toolboxes.modules.notepad.headingTitle",
|
"toolboxes.modules.notepad.headingTitle",
|
||||||
|
"toolboxes.modules.notepad.listGroupLabel",
|
||||||
"toolboxes.modules.notepad.bulletListTitle",
|
"toolboxes.modules.notepad.bulletListTitle",
|
||||||
"toolboxes.modules.notepad.numberListTitle",
|
"toolboxes.modules.notepad.numberListTitle",
|
||||||
"toolboxes.modules.notepad.colorGroupLabel",
|
"toolboxes.modules.notepad.colorGroupLabel",
|
||||||
|
|
|
||||||
|
|
@ -210,8 +210,9 @@
|
||||||
"boldTitle": "Gras",
|
"boldTitle": "Gras",
|
||||||
"italicTitle": "Italique",
|
"italicTitle": "Italique",
|
||||||
"underlineTitle": "Souligné",
|
"underlineTitle": "Souligné",
|
||||||
"paragraphTitle": "Paragraphe",
|
"strikeTitle": "Barré",
|
||||||
"headingTitle": "Titre",
|
"headingTitle": "Titre",
|
||||||
|
"listGroupLabel": "Listes",
|
||||||
"bulletListTitle": "Liste à puces",
|
"bulletListTitle": "Liste à puces",
|
||||||
"numberListTitle": "Liste numérotée",
|
"numberListTitle": "Liste numérotée",
|
||||||
"colorGroupLabel": "Couleurs",
|
"colorGroupLabel": "Couleurs",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// Rôle : fournit l'outil bloc-notes riche avec annotations dessinées.
|
// Rôle : fournit l'outil bloc-notes riche avec annotations dessinées.
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
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";
|
||||||
|
|
||||||
|
|
@ -56,6 +57,56 @@ function ToolbarButton({ active, title, children, onClick }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
|
||||||
|
return (
|
||||||
|
<CompactDropdown
|
||||||
|
className="notepad-list-dropdown"
|
||||||
|
menuClassName="notepad-list-menu"
|
||||||
|
label={textContent.listGroupLabel || "Listes"}
|
||||||
|
closeOnMouseLeave
|
||||||
|
preventMouseDown
|
||||||
|
renderTrigger={({ toggle }) => (
|
||||||
|
<button
|
||||||
|
className={`notepad-toolbar-button ${activeFormats.bulletList || activeFormats.numberList ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={activeFormats.bulletList || activeFormats.numberList}
|
||||||
|
title={textContent.listGroupLabel || "Listes"}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="notepad-list-icon" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={`notepad-list-option ${activeFormats.bulletList ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={activeFormats.bulletList}
|
||||||
|
onClick={() => {
|
||||||
|
onCommand("insertUnorderedList");
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`notepad-list-option ${activeFormats.numberList ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={activeFormats.numberList}
|
||||||
|
onClick={() => {
|
||||||
|
onCommand("insertOrderedList");
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
1.
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CompactDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function NotepadModule({ toolboxId, moduleId, context }) {
|
export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
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]);
|
||||||
|
|
@ -76,7 +127,7 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
bold: false,
|
bold: false,
|
||||||
italic: false,
|
italic: false,
|
||||||
underline: false,
|
underline: false,
|
||||||
paragraph: false,
|
strikeThrough: false,
|
||||||
heading: false,
|
heading: false,
|
||||||
bulletList: false,
|
bulletList: false,
|
||||||
numberList: false
|
numberList: false
|
||||||
|
|
@ -104,7 +155,7 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
if (!editor || !selection?.rangeCount || !editor.contains(selection.anchorNode)) {
|
if (!editor || !selection?.rangeCount || !editor.contains(selection.anchorNode)) {
|
||||||
setActiveFormats((current) => Object.values(current).some(Boolean)
|
setActiveFormats((current) => Object.values(current).some(Boolean)
|
||||||
? { bold: false, italic: false, underline: false, paragraph: false, heading: false, bulletList: false, numberList: false }
|
? { bold: false, italic: false, underline: false, strikeThrough: false, heading: false, bulletList: false, numberList: false }
|
||||||
: current);
|
: current);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +164,7 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
bold: queryCommandState("bold"),
|
bold: queryCommandState("bold"),
|
||||||
italic: queryCommandState("italic"),
|
italic: queryCommandState("italic"),
|
||||||
underline: queryCommandState("underline"),
|
underline: queryCommandState("underline"),
|
||||||
paragraph: block === "p" || block === "div",
|
strikeThrough: queryCommandState("strikeThrough"),
|
||||||
heading: block === "h3" || block === "heading 3",
|
heading: block === "h3" || block === "heading 3",
|
||||||
bulletList: queryCommandState("insertUnorderedList"),
|
bulletList: queryCommandState("insertUnorderedList"),
|
||||||
numberList: queryCommandState("insertOrderedList")
|
numberList: queryCommandState("insertOrderedList")
|
||||||
|
|
@ -206,6 +257,10 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
runCommand("formatBlock", tag);
|
runCommand("formatBlock", tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleHeading() {
|
||||||
|
applyBlock(activeFormats.heading ? "p" : "h3");
|
||||||
|
}
|
||||||
|
|
||||||
function applyTextColor(color) {
|
function applyTextColor(color) {
|
||||||
setTextColor(color);
|
setTextColor(color);
|
||||||
runCommand("foreColor", color);
|
runCommand("foreColor", color);
|
||||||
|
|
@ -253,12 +308,11 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||||
<ToolbarButton active={activeFormats.bold} title={textContent.boldTitle || "Gras"} onClick={() => runCommand("bold")}>B</ToolbarButton>
|
<ToolbarButton active={activeFormats.bold} title={textContent.boldTitle || "Gras"} onClick={() => runCommand("bold")}>B</ToolbarButton>
|
||||||
<ToolbarButton active={activeFormats.italic} title={textContent.italicTitle || "Italique"} onClick={() => runCommand("italic")}><em>I</em></ToolbarButton>
|
<ToolbarButton active={activeFormats.italic} title={textContent.italicTitle || "Italique"} onClick={() => runCommand("italic")}><em>I</em></ToolbarButton>
|
||||||
<ToolbarButton active={activeFormats.underline} title={textContent.underlineTitle || "Souligné"} onClick={() => runCommand("underline")}><u>U</u></ToolbarButton>
|
<ToolbarButton active={activeFormats.underline} title={textContent.underlineTitle || "Souligné"} onClick={() => runCommand("underline")}><u>U</u></ToolbarButton>
|
||||||
<ToolbarButton active={activeFormats.paragraph} title={textContent.paragraphTitle || "Paragraphe"} onClick={() => applyBlock("p")}>P</ToolbarButton>
|
<ToolbarButton active={activeFormats.strikeThrough} title={textContent.strikeTitle || "Barré"} onClick={() => runCommand("strikeThrough")}><s>S</s></ToolbarButton>
|
||||||
<ToolbarButton active={activeFormats.heading} title={textContent.headingTitle || "Titre"} onClick={() => applyBlock("h3")}>H</ToolbarButton>
|
<ToolbarButton active={activeFormats.heading} title={textContent.headingTitle || "Titre"} onClick={toggleHeading}>T</ToolbarButton>
|
||||||
<ToolbarButton active={activeFormats.bulletList} title={textContent.bulletListTitle || "Liste à puces"} onClick={() => runCommand("insertUnorderedList")}>•</ToolbarButton>
|
|
||||||
<ToolbarButton active={activeFormats.numberList} title={textContent.numberListTitle || "Liste numérotée"} onClick={() => runCommand("insertOrderedList")}>1.</ToolbarButton>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="notepad-toolbar-group" role="group" aria-label={textContent.colorGroupLabel || "Couleurs"}>
|
<div className="notepad-toolbar-group" role="group" aria-label={textContent.colorGroupLabel || "Couleurs"}>
|
||||||
|
<ListTypeDropdown textContent={textContent} activeFormats={activeFormats} onCommand={runCommand} />
|
||||||
<FormattingColorDropdown
|
<FormattingColorDropdown
|
||||||
label={textContent.colorGroupLabel || "Couleurs"}
|
label={textContent.colorGroupLabel || "Couleurs"}
|
||||||
sections={[
|
sections={[
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z", task: "a", relation: "e", stroke: "d" };
|
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z", task: "a", relation: "e", stroke: "d" };
|
||||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
const NOTEPAD_ALLOWED_COLORS = new Set(["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#101426", "#202745"]);
|
const NOTEPAD_ALLOWED_COLORS = new Set(["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#101426", "#202745"]);
|
||||||
const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "ul", "ol", "li", "h3", "h4", "span", "mark"]);
|
const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "s", "strike", "ul", "ol", "li", "h3", "h4", "span", "mark"]);
|
||||||
const NOTEPAD_RGB_COLOR_MAP = {
|
const NOTEPAD_RGB_COLOR_MAP = {
|
||||||
"rgb(245, 247, 255)": "#f5f7ff",
|
"rgb(245, 247, 255)": "#f5f7ff",
|
||||||
"rgb(180, 189, 211)": "#b4bdd3",
|
"rgb(180, 189, 211)": "#b4bdd3",
|
||||||
|
|
|
||||||
|
|
@ -857,6 +857,39 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notepad-list-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-icon,
|
||||||
|
.notepad-list-icon::before,
|
||||||
|
.notepad-list-icon::after {
|
||||||
|
display: block;
|
||||||
|
width: 17px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-icon {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-icon::before,
|
||||||
|
.notepad-list-icon::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-icon::before {
|
||||||
|
top: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-icon::after {
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.notepad-color-toggle::before,
|
.notepad-color-toggle::before,
|
||||||
.notepad-swatch::before {
|
.notepad-swatch::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -907,6 +940,52 @@
|
||||||
transform 140ms ease;
|
transform 140ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notepad-list-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
z-index: 50;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 34px);
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(10, 13, 28, 0.99);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-menu::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: calc(50% - 7px);
|
||||||
|
border-right: 7px solid transparent;
|
||||||
|
border-bottom: 7px solid rgba(10, 13, 28, 0.99);
|
||||||
|
border-left: 7px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-option {
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
min-width: 34px;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border-color: rgba(150, 165, 205, 0.18);
|
||||||
|
background: rgba(16, 20, 38, 0.74);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 800;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notepad-list-option:hover,
|
||||||
|
.notepad-list-option.active {
|
||||||
|
border-color: rgba(246, 196, 83, 0.48);
|
||||||
|
background: rgba(246, 196, 83, 0.1);
|
||||||
|
color: var(--color-accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
.notepad-color-menu.is-sectioned {
|
.notepad-color-menu.is-sectioned {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
width: 168px;
|
width: 168px;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue