fix(quick-260921-jt4): Notiz-Aufgabenkaestchen bedient sich selbst
- NoteCheckbox traegt jetzt ein echtes onChange und gibt sein eigenes DOM-Element an onToggle weiter; readOnly entfaellt (D-01) — war bisher nur da, um Reacts Warnung ueber ein gesteuertes Feld ohne onChange zu unterdruecken - Index-Ermittlung bleibt wortgleich (alle Kaestchen im Behaelter einsammeln, indexOf auf dem ausloesenden Element), wandert aber vom Behaelter-onClick in handleCheckboxToggle, das den Behaelter ueber ein ref statt event.currentTarget findet - previewOptions als useMemo mit leerer Abhaengigkeitsliste, Rueckruf ueber ein ref erreicht — identitaetsstabil wie die alte Modulkonstante, T-JT4-03: rehypePlugins: [[rehypeSanitize]] unveraendert erhalten - Tests: echte Tastaturbetaetigung (Leertaste auf fokussiertem Kaestchen) UND echter Klick loesen onToggle/PATCH aus; neue Tests belegen readOnly/disabled entfallen Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTheme } from 'next-themes';
|
||||
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||
@@ -13,21 +13,14 @@ const DEBOUNCE_MS = 1500;
|
||||
|
||||
// quick-260916-iex: Aufgabenlisten sind in der Ansicht direkt abhakbar —
|
||||
// `components.input` ersetzt das per rehypeSanitize deaktivierte `input`
|
||||
// durch NoteCheckbox (siehe note-task-list.tsx). Stabile Konstante ausserhalb
|
||||
// der Komponente, damit react-markdown nicht bei jedem Render neu diffed.
|
||||
// Typ direkt von MDEditor abgeleitet statt aus dem transitiven Paket
|
||||
// `@uiw/react-markdown-preview` importiert (das ist keine direkte
|
||||
// Dependency von apps/web und daher unter pnpms strikter Isolation nicht
|
||||
// von hier aus aufloesbar).
|
||||
// durch NoteCheckbox (siehe note-task-list.tsx). Typ direkt von MDEditor
|
||||
// abgeleitet statt aus dem transitiven Paket `@uiw/react-markdown-preview`
|
||||
// importiert (das ist keine direkte Dependency von apps/web und daher unter
|
||||
// pnpms strikter Isolation nicht von hier aus aufloesbar).
|
||||
type PreviewOptions = NonNullable<
|
||||
React.ComponentProps<typeof MDEditor>['previewOptions']
|
||||
>;
|
||||
|
||||
const PREVIEW_OPTIONS: PreviewOptions = {
|
||||
rehypePlugins: [[rehypeSanitize]],
|
||||
components: { input: NoteCheckbox },
|
||||
};
|
||||
|
||||
const NOTE_COMMANDS = [
|
||||
commands.bold,
|
||||
commands.italic,
|
||||
@@ -113,23 +106,27 @@ export function NoteWidget({ instanceId, config }: WidgetProps) {
|
||||
[scheduleSave, content],
|
||||
);
|
||||
|
||||
// quick-260916-iex: Klick auf ein Aufgaben-Kaestchen in der Ansicht kippt
|
||||
// genau diese Zeile und speichert sofort (kein Entprellen — ein Klick ist
|
||||
// eine abgeschlossene Handlung). Ein noch laufender Entprell-Timer aus dem
|
||||
// Tippen wird verworfen, damit er den ungekippten Text nicht nachschiebt.
|
||||
const handlePreviewClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Vorschau-Behaelter: die Index-Ermittlung (n-tes Kaestchen im Behaelter ==
|
||||
// n-te Aufgabenzeile) bleibt Wort fuer Wort wie zuvor, findet den Behaelter
|
||||
// jetzt aber ueber ein ref statt ueber event.currentTarget, weil der Klick
|
||||
// ab jetzt am Kaestchen selbst entsteht (quick-260921-jt4, D-01: das
|
||||
// Kaestchen bedient sich selbst statt vom Behaelter bedient zu werden).
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// quick-260916-iex: Abhaken eines Aufgaben-Kaestchens kippt genau diese
|
||||
// Zeile und speichert sofort (kein Entprellen — eine Tastatur-/Maus-
|
||||
// betaetigung ist eine abgeschlossene Handlung). Ein noch laufender
|
||||
// Entprell-Timer aus dem Tippen wird verworfen, damit er den ungekippten
|
||||
// Text nicht nachschiebt.
|
||||
const handleCheckboxToggle = useCallback(
|
||||
(element: HTMLInputElement) => {
|
||||
if (isEditing) return;
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLInputElement) || target.type !== 'checkbox') {
|
||||
return;
|
||||
}
|
||||
const container = previewRef.current;
|
||||
if (!container) return;
|
||||
const boxes = Array.from(
|
||||
event.currentTarget.querySelectorAll<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
),
|
||||
container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'),
|
||||
);
|
||||
const index = boxes.indexOf(target);
|
||||
const index = boxes.indexOf(element);
|
||||
const next = toggleTaskLine(content, index);
|
||||
if (next === content) return;
|
||||
setContent(next);
|
||||
@@ -139,6 +136,30 @@ export function NoteWidget({ instanceId, config }: WidgetProps) {
|
||||
[isEditing, content, title, save],
|
||||
);
|
||||
|
||||
// Der Rueckruf, den `previewOptions.components.input` letztlich aufruft,
|
||||
// haengt via ref an der jeweils aktuellen `handleCheckboxToggle` — das
|
||||
// haelt `previewOptions` selbst identitaetsstabil (useMemo mit leerer
|
||||
// Abhaengigkeitsliste), waehrend der Rueckruf trotzdem immer den frischen
|
||||
// Zustand sieht. Ein pro Tastendruck neu erzeugtes Optionsobjekt liesse
|
||||
// react-markdown bei jedem Zeichen neu abgleichen.
|
||||
const handleCheckboxToggleRef = useRef(handleCheckboxToggle);
|
||||
handleCheckboxToggleRef.current = handleCheckboxToggle;
|
||||
|
||||
const previewOptions = useMemo<PreviewOptions>(
|
||||
() => ({
|
||||
rehypePlugins: [[rehypeSanitize]],
|
||||
components: {
|
||||
input: (props: { checked?: boolean }) => (
|
||||
<NoteCheckbox
|
||||
checked={props.checked}
|
||||
onToggle={(element) => handleCheckboxToggleRef.current(element)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden" data-color-mode={colorMode}>
|
||||
{/* Header: title + pencil toggle */}
|
||||
@@ -184,7 +205,7 @@ export function NoteWidget({ instanceId, config }: WidgetProps) {
|
||||
<div
|
||||
className="flex-1 overflow-auto"
|
||||
data-testid="note-preview"
|
||||
onClick={handlePreviewClick}
|
||||
ref={previewRef}
|
||||
>
|
||||
<MDEditor
|
||||
data-testid="md-editor"
|
||||
@@ -195,7 +216,7 @@ export function NoteWidget({ instanceId, config }: WidgetProps) {
|
||||
hideToolbar={!isEditing}
|
||||
height="100%"
|
||||
visibleDragbar={false}
|
||||
previewOptions={PREVIEW_OPTIONS}
|
||||
previewOptions={previewOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user