@alpaca-editor/core 1.0.3974 → 1.0.3978
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ActionButton.js +1 -1
- package/dist/components/ActionButton.js.map +1 -1
- package/dist/components/FilterInput.d.ts +1 -0
- package/dist/components/FilterInput.js +2 -2
- package/dist/components/FilterInput.js.map +1 -1
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.js +10 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/command.js +1 -1
- package/dist/components/ui/dialog.js +2 -2
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/upload-button.d.ts +15 -0
- package/dist/components/ui/upload-button.js +56 -0
- package/dist/components/ui/upload-button.js.map +1 -0
- package/dist/config/config.js +7 -5
- package/dist/config/config.js.map +1 -1
- package/dist/editor/ContentTree.js +1 -1
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/FieldActionsOverlay.js +1 -1
- package/dist/editor/FieldActionsOverlay.js.map +1 -1
- package/dist/editor/FieldList.js +3 -3
- package/dist/editor/FieldList.js.map +1 -1
- package/dist/editor/FieldListField.js +6 -8
- package/dist/editor/FieldListField.js.map +1 -1
- package/dist/editor/FieldListFieldWithFallbacks.js +2 -1
- package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
- package/dist/editor/ImageEditor.js +1 -1
- package/dist/editor/ImageEditor.js.map +1 -1
- package/dist/editor/ItemInfo.js +4 -4
- package/dist/editor/ItemInfo.js.map +1 -1
- package/dist/editor/commands/componentCommands.js +2 -2
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/context-menu/InsertMenu.js +1 -1
- package/dist/editor/context-menu/InsertMenu.js.map +1 -1
- package/dist/editor/field-types/CheckboxEditor.js +3 -3
- package/dist/editor/field-types/CheckboxEditor.js.map +1 -1
- package/dist/editor/field-types/ImageFieldEditor.js +1 -1
- package/dist/editor/field-types/InternalLinkFieldEditor.js +3 -2
- package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
- package/dist/editor/field-types/LinkFieldEditor.js +1 -1
- package/dist/editor/field-types/LinkFieldEditor.js.map +1 -1
- package/dist/editor/field-types/RawEditor.js +1 -1
- package/dist/editor/field-types/RawEditor.js.map +1 -1
- package/dist/editor/field-types/SingleLineText.js +1 -1
- package/dist/editor/fieldTypes.d.ts +1 -0
- package/dist/editor/media-selector/AiImageSearch.js.map +1 -1
- package/dist/editor/media-selector/AiImageSearchPrompt.js +3 -4
- package/dist/editor/media-selector/AiImageSearchPrompt.js.map +1 -1
- package/dist/editor/media-selector/MediaSelector.js +3 -3
- package/dist/editor/media-selector/MediaSelector.js.map +1 -1
- package/dist/editor/media-selector/Preview.d.ts +1 -1
- package/dist/editor/media-selector/Preview.js +14 -2
- package/dist/editor/media-selector/Preview.js.map +1 -1
- package/dist/editor/media-selector/Thumbnails.d.ts +1 -6
- package/dist/editor/media-selector/Thumbnails.js +3 -2
- package/dist/editor/media-selector/Thumbnails.js.map +1 -1
- package/dist/editor/media-selector/TreeSelector.js +22 -17
- package/dist/editor/media-selector/TreeSelector.js.map +1 -1
- package/dist/editor/media-selector/UploadZone.js +5 -3
- package/dist/editor/media-selector/UploadZone.js.map +1 -1
- package/dist/editor/media-selector/index.d.ts +1 -1
- package/dist/editor/media-selector/index.js.map +1 -1
- package/dist/editor/menubar/GenericToolbar.js +1 -1
- package/dist/editor/menubar/GenericToolbar.js.map +1 -1
- package/dist/editor/menubar/ToolbarFactory.js +2 -2
- package/dist/editor/menubar/ToolbarFactory.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/ReviewCommands.js +19 -19
- package/dist/editor/menubar/toolbar-sections/ReviewCommands.js.map +1 -1
- package/dist/editor/page-viewer/PageViewer.js +1 -1
- package/dist/editor/page-viewer/PageViewer.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +1 -7
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/reviews/Comment.js +14 -18
- package/dist/editor/reviews/Comment.js.map +1 -1
- package/dist/editor/reviews/SuggestedEdit.js +6 -5
- package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
- package/dist/editor/services/contentService.d.ts +12 -1
- package/dist/editor/services/contentService.js.map +1 -1
- package/dist/editor/sidebar/SidebarView.js +4 -2
- package/dist/editor/sidebar/SidebarView.js.map +1 -1
- package/dist/editor/ui/PerfectTree.d.ts +8 -3
- package/dist/editor/ui/PerfectTree.js +215 -8
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/editor/ui/Section.js +2 -1
- package/dist/editor/ui/Section.js.map +1 -1
- package/dist/page-wizard/PageWizard.d.ts +1 -1
- package/dist/page-wizard/steps/Components.d.ts +1 -1
- package/dist/page-wizard/steps/Components.js +2 -2
- package/dist/page-wizard/steps/Components.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +81 -16
- package/package.json +2 -1
- package/src/components/ActionButton.tsx +2 -0
- package/src/components/FilterInput.tsx +7 -1
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/dialog.tsx +5 -2
- package/src/components/ui/upload-button.tsx +117 -0
- package/src/config/config.tsx +11 -6
- package/src/editor/ContentTree.tsx +1 -0
- package/src/editor/FieldActionsOverlay.tsx +1 -1
- package/src/editor/FieldList.tsx +14 -10
- package/src/editor/FieldListField.tsx +20 -18
- package/src/editor/FieldListFieldWithFallbacks.tsx +14 -11
- package/src/editor/ImageEditor.tsx +1 -1
- package/src/editor/ItemInfo.tsx +10 -10
- package/src/editor/commands/componentCommands.tsx +2 -1
- package/src/editor/context-menu/InsertMenu.tsx +2 -1
- package/src/editor/field-types/CheckboxEditor.tsx +3 -3
- package/src/editor/field-types/ImageFieldEditor.tsx +1 -1
- package/src/editor/field-types/InternalLinkFieldEditor.tsx +8 -4
- package/src/editor/field-types/LinkFieldEditor.tsx +1 -1
- package/src/editor/field-types/RawEditor.tsx +1 -1
- package/src/editor/field-types/SingleLineText.tsx +1 -1
- package/src/editor/fieldTypes.ts +1 -0
- package/src/editor/media-selector/AiImageSearch.tsx +2 -1
- package/src/editor/media-selector/AiImageSearchPrompt.tsx +39 -20
- package/src/editor/media-selector/MediaFolderBrowser.tsx +1 -1
- package/src/editor/media-selector/MediaSelector.tsx +26 -17
- package/src/editor/media-selector/Preview.tsx +41 -3
- package/src/editor/media-selector/Thumbnails.tsx +13 -14
- package/src/editor/media-selector/TreeSelector.tsx +94 -40
- package/src/editor/media-selector/UploadZone.tsx +14 -12
- package/src/editor/media-selector/index.ts +1 -1
- package/src/editor/menubar/GenericToolbar.tsx +1 -3
- package/src/editor/menubar/ToolbarFactory.tsx +2 -2
- package/src/editor/menubar/toolbar-sections/ReviewCommands.tsx +7 -1
- package/src/editor/page-viewer/PageViewer.tsx +1 -1
- package/src/editor/page-viewer/PageViewerFrame.tsx +1 -10
- package/src/editor/reviews/Comment.tsx +51 -43
- package/src/editor/reviews/SuggestedEdit.tsx +30 -19
- package/src/editor/services/contentService.ts +13 -1
- package/src/editor/sidebar/SidebarView.tsx +8 -6
- package/src/editor/ui/PerfectTree.tsx +305 -9
- package/src/editor/ui/Section.tsx +16 -6
- package/src/page-wizard/PageWizard.tsx +1 -1
- package/src/page-wizard/steps/Components.tsx +8 -10
- package/src/revision.ts +2 -2
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
import { Button } from "../../components/ui/button";
|
|
11
11
|
import { formatDate } from "../utils";
|
|
12
12
|
import { SimpleIconButton } from "../ui/SimpleIconButton";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Popover,
|
|
15
|
+
PopoverContent,
|
|
16
|
+
PopoverTrigger,
|
|
17
|
+
} from "../../components/ui/popover";
|
|
14
18
|
import { ProgressSpinner } from "primereact/progressspinner";
|
|
15
19
|
import { useDebouncedCallback } from "use-debounce";
|
|
16
20
|
import { ActionButton } from "../../components/ActionButton";
|
|
@@ -20,6 +24,8 @@ export function Comment({ comment }: { comment: CommentType }) {
|
|
|
20
24
|
const [commentText, setCommentText] = useState(comment.text);
|
|
21
25
|
const [isEditing, setIsEditing] = useState(false);
|
|
22
26
|
const [isSaving, setIsSaving] = useState(false);
|
|
27
|
+
const [deletePopoverOpen, setDeletePopoverOpen] = useState(false);
|
|
28
|
+
const [resolvePopoverOpen, setResolvePopoverOpen] = useState(false);
|
|
23
29
|
const ref = useRef<HTMLDivElement>(null);
|
|
24
30
|
|
|
25
31
|
useEffect(() => {
|
|
@@ -90,8 +96,6 @@ export function Comment({ comment }: { comment: CommentType }) {
|
|
|
90
96
|
};
|
|
91
97
|
|
|
92
98
|
const renderHeader = () => {
|
|
93
|
-
const overlayPanelRef = useRef<OverlayPanel>(null);
|
|
94
|
-
const deleteOverlayPanelRef = useRef<OverlayPanel>(null);
|
|
95
99
|
return (
|
|
96
100
|
<>
|
|
97
101
|
<div className="mb-3 flex items-start justify-between">
|
|
@@ -115,25 +119,29 @@ export function Comment({ comment }: { comment: CommentType }) {
|
|
|
115
119
|
/>
|
|
116
120
|
)}
|
|
117
121
|
{canDelete && (
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
onClick={(e) => {
|
|
122
|
-
deleteOverlayPanelRef.current?.toggle(e);
|
|
123
|
-
}}
|
|
124
|
-
/>
|
|
125
|
-
)}
|
|
126
|
-
<OverlayPanel ref={deleteOverlayPanelRef}>
|
|
127
|
-
<Button
|
|
128
|
-
className="m-2"
|
|
129
|
-
variant="outline"
|
|
130
|
-
onClick={async () => {
|
|
131
|
-
await deleteComment(comment);
|
|
132
|
-
}}
|
|
122
|
+
<Popover
|
|
123
|
+
open={deletePopoverOpen}
|
|
124
|
+
onOpenChange={setDeletePopoverOpen}
|
|
133
125
|
>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
<PopoverTrigger asChild>
|
|
127
|
+
<button
|
|
128
|
+
className="pi pi-trash hover:bg-gray-5 cursor-pointer rounded-full p-[6px] text-xs"
|
|
129
|
+
title="Delete"
|
|
130
|
+
/>
|
|
131
|
+
</PopoverTrigger>
|
|
132
|
+
<PopoverContent className="w-auto p-2" align="end">
|
|
133
|
+
<Button
|
|
134
|
+
variant="outline"
|
|
135
|
+
onClick={async () => {
|
|
136
|
+
await deleteComment(comment);
|
|
137
|
+
setDeletePopoverOpen(false);
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
Delete
|
|
141
|
+
</Button>
|
|
142
|
+
</PopoverContent>
|
|
143
|
+
</Popover>
|
|
144
|
+
)}
|
|
137
145
|
{canResolve && !comment.isResolved && (
|
|
138
146
|
<SimpleIconButton
|
|
139
147
|
icon="pi pi-check"
|
|
@@ -144,35 +152,35 @@ export function Comment({ comment }: { comment: CommentType }) {
|
|
|
144
152
|
/>
|
|
145
153
|
)}
|
|
146
154
|
{comment.isResolved && (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
<Popover
|
|
156
|
+
open={resolvePopoverOpen}
|
|
157
|
+
onOpenChange={setResolvePopoverOpen}
|
|
158
|
+
>
|
|
159
|
+
<PopoverTrigger asChild>
|
|
160
|
+
<i
|
|
161
|
+
className="pi pi-check cursor-pointer px-1 text-xs text-green-500"
|
|
162
|
+
style={{ fontWeight: "bold" }}
|
|
163
|
+
title={
|
|
164
|
+
"Resolved by " +
|
|
165
|
+
comment.resolvedBy +
|
|
166
|
+
" (" +
|
|
167
|
+
formatDate(new Date(comment.resolvedDate!)) +
|
|
168
|
+
")"
|
|
161
169
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
170
|
+
/>
|
|
171
|
+
</PopoverTrigger>
|
|
172
|
+
<PopoverContent className="w-auto p-2" align="end">
|
|
165
173
|
<Button
|
|
166
|
-
className="m-2"
|
|
167
174
|
variant="outline"
|
|
168
175
|
onClick={async () => {
|
|
169
176
|
await unresolveComment(comment);
|
|
177
|
+
setResolvePopoverOpen(false);
|
|
170
178
|
}}
|
|
171
179
|
>
|
|
172
180
|
Unresolve
|
|
173
181
|
</Button>
|
|
174
|
-
</
|
|
175
|
-
|
|
182
|
+
</PopoverContent>
|
|
183
|
+
</Popover>
|
|
176
184
|
)}
|
|
177
185
|
{canResolve && !comment.isResolved && (
|
|
178
186
|
<SimpleIconButton
|
|
@@ -249,7 +257,7 @@ export function Comment({ comment }: { comment: CommentType }) {
|
|
|
249
257
|
autoFocus
|
|
250
258
|
/>
|
|
251
259
|
{isSaving && (
|
|
252
|
-
<div className="mt-1 flex justify-end gap-2">
|
|
260
|
+
<div className="flex-wra mt-1 flex justify-end gap-2">
|
|
253
261
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
254
262
|
<ProgressSpinner className="h-4 w-4" />
|
|
255
263
|
Saving...
|
|
@@ -257,7 +265,7 @@ export function Comment({ comment }: { comment: CommentType }) {
|
|
|
257
265
|
</div>
|
|
258
266
|
)}
|
|
259
267
|
{!isSaving && (
|
|
260
|
-
<div className="mt-1 flex justify-end gap-2">
|
|
268
|
+
<div className="mt-1 flex flex-wrap justify-end gap-2">
|
|
261
269
|
<ActionButton
|
|
262
270
|
variant="outline"
|
|
263
271
|
onClick={() => {
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
import { Button } from "../../components/ui/button";
|
|
9
9
|
import { formatDate } from "../utils";
|
|
10
10
|
import { SimpleIconButton } from "../ui/SimpleIconButton";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Popover,
|
|
13
|
+
PopoverContent,
|
|
14
|
+
PopoverTrigger,
|
|
15
|
+
} from "../../components/ui/popover";
|
|
12
16
|
import { DiffView } from "./DiffView";
|
|
13
17
|
// Import lucide icons (adjust names as needed)
|
|
14
18
|
import { Trash2, GalleryVertical, Check, Brush, XCircle } from "lucide-react";
|
|
@@ -19,9 +23,9 @@ import { cn } from "../../lib/utils";
|
|
|
19
23
|
export function SuggestedEditComponent({ edit }: { edit: SuggestedEditType }) {
|
|
20
24
|
const editContext = useEditContext();
|
|
21
25
|
const ref = useRef<HTMLDivElement>(null);
|
|
22
|
-
const overlayPanelRef = useRef<OverlayPanel>(null);
|
|
23
26
|
const [item, setItem] = useState<any>(null);
|
|
24
27
|
const [patchPossible, setPatchPossible] = useState<boolean>(true);
|
|
28
|
+
const [deletePopoverOpen, setDeletePopoverOpen] = useState(false);
|
|
25
29
|
|
|
26
30
|
const [patchWarning, setPatchWarning] = useState<string>("");
|
|
27
31
|
const [applied, setApplied] = useState<boolean>(false);
|
|
@@ -151,24 +155,31 @@ export function SuggestedEditComponent({ edit }: { edit: SuggestedEditType }) {
|
|
|
151
155
|
></i>
|
|
152
156
|
)}
|
|
153
157
|
{canDelete && (
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
label="Delete"
|
|
158
|
-
onClick={(e: any) => overlayPanelRef.current?.toggle(e)}
|
|
159
|
-
/>
|
|
160
|
-
)}
|
|
161
|
-
<OverlayPanel ref={overlayPanelRef}>
|
|
162
|
-
<Button
|
|
163
|
-
className="m-2"
|
|
164
|
-
variant="outline"
|
|
165
|
-
onClick={async () => {
|
|
166
|
-
await deleteSuggestedEdit(edit);
|
|
167
|
-
}}
|
|
158
|
+
<Popover
|
|
159
|
+
open={deletePopoverOpen}
|
|
160
|
+
onOpenChange={setDeletePopoverOpen}
|
|
168
161
|
>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
<PopoverTrigger asChild>
|
|
163
|
+
<button
|
|
164
|
+
className="cursor-pointer rounded-full p-1 text-gray-500 hover:bg-gray-100"
|
|
165
|
+
title="Delete"
|
|
166
|
+
>
|
|
167
|
+
<Trash2 size={14} />
|
|
168
|
+
</button>
|
|
169
|
+
</PopoverTrigger>
|
|
170
|
+
<PopoverContent className="w-auto p-2" align="end">
|
|
171
|
+
<Button
|
|
172
|
+
variant="outline"
|
|
173
|
+
onClick={async () => {
|
|
174
|
+
await deleteSuggestedEdit(edit);
|
|
175
|
+
setDeletePopoverOpen(false);
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
Delete
|
|
179
|
+
</Button>
|
|
180
|
+
</PopoverContent>
|
|
181
|
+
</Popover>
|
|
182
|
+
)}
|
|
172
183
|
</div>
|
|
173
184
|
</div>
|
|
174
185
|
);
|
|
@@ -10,7 +10,19 @@ import {
|
|
|
10
10
|
} from "../../types";
|
|
11
11
|
import { FullItem, ItemDescriptor, ItemStub } from "../pageModel";
|
|
12
12
|
|
|
13
|
-
export type
|
|
13
|
+
export type Thumbnail = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
thumbUrl: string;
|
|
17
|
+
previewUrl: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
updated?: string;
|
|
22
|
+
updatedBy?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ItemTreeNodeData = Thumbnail & {
|
|
14
26
|
id: string;
|
|
15
27
|
name: string;
|
|
16
28
|
displayName?: string;
|
|
@@ -3,6 +3,8 @@ import { classNames } from "primereact/utils";
|
|
|
3
3
|
import { Sidebar } from "../../config/types";
|
|
4
4
|
import { Splitter, SplitterPanel } from "../ui/Splitter";
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
|
+
import { SimpleIconButton } from "../ui/SimpleIconButton";
|
|
7
|
+
import { X } from "lucide-react";
|
|
6
8
|
|
|
7
9
|
export function SidebarView({
|
|
8
10
|
sidebar,
|
|
@@ -39,12 +41,12 @@ export function SidebarView({
|
|
|
39
41
|
|
|
40
42
|
{/* Close button - only show on the first panel */}
|
|
41
43
|
{index === 0 && (
|
|
42
|
-
<
|
|
44
|
+
<SimpleIconButton
|
|
43
45
|
onClick={onClose}
|
|
44
|
-
className="
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
icon={<X className="size-4" />}
|
|
47
|
+
label="Close"
|
|
48
|
+
className="ml-2"
|
|
49
|
+
/>
|
|
48
50
|
)}
|
|
49
51
|
</div>
|
|
50
52
|
);
|
|
@@ -66,7 +68,7 @@ export function SidebarView({
|
|
|
66
68
|
<div
|
|
67
69
|
className={cn(
|
|
68
70
|
"flex h-full flex-col bg-white",
|
|
69
|
-
detached ? "border-gray-3 rounded-
|
|
71
|
+
detached ? "border-gray-3 rounded-md border" : "",
|
|
70
72
|
)}
|
|
71
73
|
>
|
|
72
74
|
{getHeader(panel, 0)}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useMemo,
|
|
4
|
+
useCallback,
|
|
5
|
+
memo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
2
9
|
|
|
3
10
|
import { ProgressSpinner } from "primereact/progressspinner";
|
|
4
11
|
import { ChevronRight } from "lucide-react";
|
|
@@ -25,7 +32,7 @@ export interface TreeProps<T = any> {
|
|
|
25
32
|
/** Keys of expanded nodes */
|
|
26
33
|
expandedKeys?: string[];
|
|
27
34
|
/** Callback to render a single node (template) */
|
|
28
|
-
renderNode: (node: TreeNode<T
|
|
35
|
+
renderNode: (node: TreeNode<T>, searchTerm?: string) => React.ReactNode;
|
|
29
36
|
/** Called when a node's expand/collapse toggle is activated */
|
|
30
37
|
onToggleExpand?: (key: string) => void;
|
|
31
38
|
/** Called when a node is clicked for selection */
|
|
@@ -70,8 +77,35 @@ export interface TreeProps<T = any> {
|
|
|
70
77
|
onContextMenu?: (node: TreeNode<T>, event: React.MouseEvent) => void;
|
|
71
78
|
/** Whether to automatically scroll to the first selected node when selection changes */
|
|
72
79
|
scrollToSelected?: boolean;
|
|
80
|
+
/** Whether to enable keyboard search functionality */
|
|
81
|
+
enableKeyboardSearch?: boolean;
|
|
82
|
+
/** Time in ms before search is cleared (default: 1500) */
|
|
83
|
+
searchClearDelay?: number;
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Usage example with keyboard search:
|
|
88
|
+
*
|
|
89
|
+
* ```tsx
|
|
90
|
+
* import { PerfectTree } from './PerfectTree';
|
|
91
|
+
*
|
|
92
|
+
* <PerfectTree
|
|
93
|
+
* nodes={treeNodes}
|
|
94
|
+
* enableKeyboardSearch={true}
|
|
95
|
+
* renderNode={(node) => (
|
|
96
|
+
* <div>
|
|
97
|
+
* {node.icon}
|
|
98
|
+
* {node.label}
|
|
99
|
+
* </div>
|
|
100
|
+
* )}
|
|
101
|
+
* // ... other props
|
|
102
|
+
* />
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* Note: Text highlighting is applied automatically when enableKeyboardSearch is true.
|
|
106
|
+
* The highlightText helper function is still available for manual highlighting if needed.
|
|
107
|
+
*/
|
|
108
|
+
|
|
75
109
|
// Local DropZone component to handle drag-over state.
|
|
76
110
|
const DropZone = memo(
|
|
77
111
|
({
|
|
@@ -179,6 +213,132 @@ const DropZone = memo(
|
|
|
179
213
|
},
|
|
180
214
|
);
|
|
181
215
|
|
|
216
|
+
// Helper function to highlight matching text
|
|
217
|
+
export const highlightText = (
|
|
218
|
+
text: string,
|
|
219
|
+
searchTerm: string,
|
|
220
|
+
): React.ReactNode => {
|
|
221
|
+
if (!searchTerm.trim()) return text;
|
|
222
|
+
|
|
223
|
+
const regex = new RegExp(
|
|
224
|
+
`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
|
|
225
|
+
"gi",
|
|
226
|
+
);
|
|
227
|
+
const parts = text.split(regex);
|
|
228
|
+
|
|
229
|
+
return parts.map((part, index) => {
|
|
230
|
+
if (regex.test(part)) {
|
|
231
|
+
return (
|
|
232
|
+
<span key={index} className="bg-yellow-200 underline">
|
|
233
|
+
{part}
|
|
234
|
+
</span>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return part;
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Helper function to highlight text within React elements
|
|
242
|
+
const highlightReactElement = (
|
|
243
|
+
element: React.ReactElement,
|
|
244
|
+
searchTerm: string,
|
|
245
|
+
nodeLabel: string,
|
|
246
|
+
): React.ReactElement => {
|
|
247
|
+
try {
|
|
248
|
+
// If searchTerm doesn't match the node label, return as-is
|
|
249
|
+
if (!nodeLabel.toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
250
|
+
return element;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const processChildren = (children: React.ReactNode): React.ReactNode => {
|
|
254
|
+
return React.Children.map(children, (child) => {
|
|
255
|
+
if (typeof child === "string") {
|
|
256
|
+
// If it's a string and contains the search term, highlight it
|
|
257
|
+
if (child.toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
258
|
+
return highlightText(child, searchTerm);
|
|
259
|
+
}
|
|
260
|
+
return child;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (React.isValidElement(child)) {
|
|
264
|
+
// Recursively process React elements
|
|
265
|
+
const childProps = (child as any).props;
|
|
266
|
+
if (childProps && childProps.children) {
|
|
267
|
+
return React.cloneElement(child as any, {
|
|
268
|
+
...childProps,
|
|
269
|
+
children: processChildren(childProps.children),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return child;
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Clone the element with processed children
|
|
279
|
+
const elementProps = (element as any).props;
|
|
280
|
+
if (elementProps && elementProps.children) {
|
|
281
|
+
return React.cloneElement(element as any, {
|
|
282
|
+
...elementProps,
|
|
283
|
+
children: processChildren(elementProps.children),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// If any error occurs, return the original element
|
|
288
|
+
console.warn("Error highlighting React element:", error);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return element;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Helper function to check if a node matches the search term
|
|
295
|
+
const nodeMatchesSearch = (
|
|
296
|
+
node: TreeNode<any>,
|
|
297
|
+
searchTerm: string,
|
|
298
|
+
): boolean => {
|
|
299
|
+
if (!searchTerm.trim()) return true;
|
|
300
|
+
return node.label.toLowerCase().includes(searchTerm.toLowerCase());
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Helper function to filter tree nodes based on search term
|
|
304
|
+
const filterTreeNodes = (
|
|
305
|
+
nodes: TreeNode<any>[],
|
|
306
|
+
searchTerm: string,
|
|
307
|
+
expandedKeys: string[],
|
|
308
|
+
): TreeNode<any>[] => {
|
|
309
|
+
if (!searchTerm.trim()) return nodes;
|
|
310
|
+
|
|
311
|
+
const filterNode = (node: TreeNode<any>): TreeNode<any> | null => {
|
|
312
|
+
const nodeMatches = nodeMatchesSearch(node, searchTerm);
|
|
313
|
+
const isExpanded = expandedKeys.includes(node.key);
|
|
314
|
+
|
|
315
|
+
// Process children only if the node is expanded
|
|
316
|
+
let filteredChildren: TreeNode<any>[] = [];
|
|
317
|
+
let hasMatchingChildren = false;
|
|
318
|
+
|
|
319
|
+
if (isExpanded && node.children && Array.isArray(node.children)) {
|
|
320
|
+
filteredChildren = node.children
|
|
321
|
+
.map((child) => filterNode(child))
|
|
322
|
+
.filter((child): child is TreeNode<any> => child !== null);
|
|
323
|
+
hasMatchingChildren = filteredChildren.length > 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Include node if it matches or has matching children
|
|
327
|
+
if (nodeMatches || hasMatchingChildren) {
|
|
328
|
+
return {
|
|
329
|
+
...node,
|
|
330
|
+
children: isExpanded ? filteredChildren : node.children,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return null;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return nodes
|
|
338
|
+
.map((node) => filterNode(node))
|
|
339
|
+
.filter((node): node is TreeNode<any> => node !== null);
|
|
340
|
+
};
|
|
341
|
+
|
|
182
342
|
// NodeContent component extracted and memoized
|
|
183
343
|
const NodeContent = memo(
|
|
184
344
|
({
|
|
@@ -197,6 +357,7 @@ const NodeContent = memo(
|
|
|
197
357
|
enableDragAndDrop = false,
|
|
198
358
|
selectedKeys,
|
|
199
359
|
isDragging,
|
|
360
|
+
searchTerm = "",
|
|
200
361
|
}: {
|
|
201
362
|
node: TreeNode<any>;
|
|
202
363
|
isExpanded: boolean;
|
|
@@ -221,10 +382,11 @@ const NodeContent = memo(
|
|
|
221
382
|
) => void;
|
|
222
383
|
onDoubleClick?: (node: TreeNode<any>) => void;
|
|
223
384
|
onContextMenu?: (node: TreeNode<any>, event: React.MouseEvent) => void;
|
|
224
|
-
renderNode: (node: TreeNode<any
|
|
385
|
+
renderNode: (node: TreeNode<any>, searchTerm?: string) => React.ReactNode;
|
|
225
386
|
enableDragAndDrop?: boolean;
|
|
226
387
|
selectedKeys?: string[];
|
|
227
388
|
isDragging: boolean;
|
|
389
|
+
searchTerm?: string;
|
|
228
390
|
}) => {
|
|
229
391
|
const [isDragOver, setIsDragOver] = React.useState(false);
|
|
230
392
|
|
|
@@ -391,7 +553,7 @@ const NodeContent = memo(
|
|
|
391
553
|
}`}
|
|
392
554
|
onClick={handleSelect}
|
|
393
555
|
>
|
|
394
|
-
{renderNode(node)}
|
|
556
|
+
{renderNode(node, searchTerm)}
|
|
395
557
|
</div>
|
|
396
558
|
</div>
|
|
397
559
|
);
|
|
@@ -416,7 +578,13 @@ export const PerfectTree = <T,>({
|
|
|
416
578
|
enableDragAndDrop = false,
|
|
417
579
|
isValidDropZone,
|
|
418
580
|
scrollToSelected = false,
|
|
581
|
+
enableKeyboardSearch = false,
|
|
582
|
+
searchClearDelay = 1500,
|
|
419
583
|
}: TreeProps<T>) => {
|
|
584
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
585
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
586
|
+
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
587
|
+
|
|
420
588
|
// When toggling a node, notify parent and trigger external lazy load if needed.
|
|
421
589
|
const handleToggle = useCallback(
|
|
422
590
|
(node: TreeNode<T>) => {
|
|
@@ -584,6 +752,42 @@ export const PerfectTree = <T,>({
|
|
|
584
752
|
prevSelectedKeysRef.current = [...selectedKeys];
|
|
585
753
|
}, [scrollToSelected, selectedKeys]);
|
|
586
754
|
|
|
755
|
+
// Enhanced renderNode function that handles highlighting
|
|
756
|
+
const enhancedRenderNode = useCallback(
|
|
757
|
+
(node: TreeNode<T>, searchTermForNode = searchTerm) => {
|
|
758
|
+
// Get the original content from renderNode
|
|
759
|
+
const originalContent = renderNode(node, searchTermForNode);
|
|
760
|
+
|
|
761
|
+
// If there's no search term, return original content
|
|
762
|
+
if (!searchTermForNode?.trim()) {
|
|
763
|
+
return originalContent;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Auto-highlight if the content is simple text
|
|
767
|
+
if (typeof originalContent === "string") {
|
|
768
|
+
return highlightText(originalContent, searchTermForNode);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// For React elements, try to find and highlight text content
|
|
772
|
+
if (React.isValidElement(originalContent)) {
|
|
773
|
+
return highlightReactElement(
|
|
774
|
+
originalContent,
|
|
775
|
+
searchTermForNode,
|
|
776
|
+
node.label,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Fallback: if we can't process the content, return original
|
|
781
|
+
return originalContent;
|
|
782
|
+
},
|
|
783
|
+
[renderNode, searchTerm],
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// Filter nodes based on search term
|
|
787
|
+
const filteredNodes = useMemo(() => {
|
|
788
|
+
return filterTreeNodes(nodes, searchTerm, expandedKeys);
|
|
789
|
+
}, [nodes, searchTerm, expandedKeys]);
|
|
790
|
+
|
|
587
791
|
// Recursive function to render tree nodes along with drop zones.
|
|
588
792
|
const renderTreeList = useCallback(
|
|
589
793
|
(
|
|
@@ -627,10 +831,11 @@ export const PerfectTree = <T,>({
|
|
|
627
831
|
onDrop={onDrop}
|
|
628
832
|
onDoubleClick={onDoubleClick}
|
|
629
833
|
onContextMenu={onContextMenu}
|
|
630
|
-
renderNode={
|
|
834
|
+
renderNode={enhancedRenderNode}
|
|
631
835
|
enableDragAndDrop={enableDragAndDrop}
|
|
632
836
|
selectedKeys={selectedKeys}
|
|
633
837
|
isDragging={isDragging}
|
|
838
|
+
searchTerm={searchTerm}
|
|
634
839
|
/>
|
|
635
840
|
{isExpanded && (
|
|
636
841
|
<>
|
|
@@ -667,17 +872,108 @@ export const PerfectTree = <T,>({
|
|
|
667
872
|
onDoubleClick,
|
|
668
873
|
handleSelect,
|
|
669
874
|
handleToggle,
|
|
670
|
-
|
|
875
|
+
enhancedRenderNode,
|
|
876
|
+
searchTerm,
|
|
671
877
|
],
|
|
672
878
|
);
|
|
673
879
|
|
|
674
880
|
// Memoize the tree structure
|
|
675
881
|
const treeContent = useMemo(
|
|
676
|
-
() => renderTreeList(
|
|
677
|
-
[
|
|
882
|
+
() => renderTreeList(filteredNodes, 0),
|
|
883
|
+
[filteredNodes, renderTreeList],
|
|
678
884
|
);
|
|
679
885
|
|
|
680
|
-
|
|
886
|
+
// Keyboard search functionality
|
|
887
|
+
useEffect(() => {
|
|
888
|
+
if (!enableKeyboardSearch || !isFocused) return;
|
|
889
|
+
|
|
890
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
891
|
+
// Ignore if user is typing in an input, textarea, or contenteditable element
|
|
892
|
+
const target = event.target as HTMLElement;
|
|
893
|
+
if (
|
|
894
|
+
target.tagName === "INPUT" ||
|
|
895
|
+
target.tagName === "TEXTAREA" ||
|
|
896
|
+
target.contentEditable === "true" ||
|
|
897
|
+
target.isContentEditable
|
|
898
|
+
) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Handle different key types
|
|
903
|
+
if (event.key === "Escape") {
|
|
904
|
+
// Clear search on Escape
|
|
905
|
+
setSearchTerm("");
|
|
906
|
+
if (searchTimeoutRef.current) {
|
|
907
|
+
clearTimeout(searchTimeoutRef.current);
|
|
908
|
+
searchTimeoutRef.current = null;
|
|
909
|
+
}
|
|
910
|
+
event.preventDefault();
|
|
911
|
+
} else if (event.key === "Backspace") {
|
|
912
|
+
// Remove last character on Backspace
|
|
913
|
+
if (searchTerm.length > 0) {
|
|
914
|
+
setSearchTerm((prev) => prev.slice(0, -1));
|
|
915
|
+
resetSearchTimeout();
|
|
916
|
+
event.preventDefault();
|
|
917
|
+
}
|
|
918
|
+
} else if (
|
|
919
|
+
event.key.length === 1 &&
|
|
920
|
+
!event.ctrlKey &&
|
|
921
|
+
!event.metaKey &&
|
|
922
|
+
!event.altKey
|
|
923
|
+
) {
|
|
924
|
+
// Add character for printable keys (letters, numbers, symbols)
|
|
925
|
+
setSearchTerm((prev) => prev + event.key);
|
|
926
|
+
resetSearchTimeout();
|
|
927
|
+
event.preventDefault();
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const resetSearchTimeout = () => {
|
|
932
|
+
if (searchTimeoutRef.current) {
|
|
933
|
+
clearTimeout(searchTimeoutRef.current);
|
|
934
|
+
}
|
|
935
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
936
|
+
setSearchTerm("");
|
|
937
|
+
searchTimeoutRef.current = null;
|
|
938
|
+
}, searchClearDelay);
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
// Add event listener to document
|
|
942
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
943
|
+
|
|
944
|
+
return () => {
|
|
945
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
946
|
+
if (searchTimeoutRef.current) {
|
|
947
|
+
clearTimeout(searchTimeoutRef.current);
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
}, [enableKeyboardSearch, isFocused, searchTerm, searchClearDelay]);
|
|
951
|
+
|
|
952
|
+
return (
|
|
953
|
+
<div
|
|
954
|
+
ref={treeRef}
|
|
955
|
+
className="perfect-tree focus:outline-none"
|
|
956
|
+
tabIndex={0}
|
|
957
|
+
onFocus={() => setIsFocused(true)}
|
|
958
|
+
onBlur={() => {
|
|
959
|
+
setIsFocused(false);
|
|
960
|
+
setSearchTerm("");
|
|
961
|
+
if (searchTimeoutRef.current) {
|
|
962
|
+
clearTimeout(searchTimeoutRef.current);
|
|
963
|
+
searchTimeoutRef.current = null;
|
|
964
|
+
}
|
|
965
|
+
}}
|
|
966
|
+
>
|
|
967
|
+
{enableKeyboardSearch && searchTerm && (
|
|
968
|
+
<div className="mb-2 flex items-center px-2 py-1 text-xs">
|
|
969
|
+
<span className="text-gray-2">Filter:</span>
|
|
970
|
+
<span className="ml-1">{searchTerm}</span>
|
|
971
|
+
<span className="text-gray-2 ml-1">(ESC to clear)</span>
|
|
972
|
+
</div>
|
|
973
|
+
)}
|
|
974
|
+
{treeContent}
|
|
975
|
+
</div>
|
|
976
|
+
);
|
|
681
977
|
};
|
|
682
978
|
|
|
683
979
|
export default memo(PerfectTree);
|