forked from github/plane
fix: slash command scroll posiiton
This commit is contained in:
parent
bcc1131ec1
commit
78bb3085f3
@ -32,10 +32,9 @@ type FormData = {
|
|||||||
task: string;
|
task: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TiptapEditor = React.forwardRef<
|
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
||||||
ITiptapRichTextEditor,
|
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
|
||||||
ITiptapRichTextEditor
|
);
|
||||||
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
|
|
||||||
|
|
||||||
TiptapEditor.displayName = "TiptapEditor";
|
TiptapEditor.displayName = "TiptapEditor";
|
||||||
|
|
||||||
@ -141,11 +140,12 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
|
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||||
}`}
|
isOpen ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
||||||
<div className="remirror-section text-sm">
|
<div id="tiptap-container" className="remirror-section text-sm">
|
||||||
Content:
|
Content:
|
||||||
<TiptapEditor
|
<TiptapEditor
|
||||||
value={htmlContent ?? `<p>${content}</p>`}
|
value={htmlContent ?? `<p>${content}</p>`}
|
||||||
@ -179,10 +179,11 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
name="task"
|
name="task"
|
||||||
register={register}
|
register={register}
|
||||||
placeholder={`${content && content !== ""
|
placeholder={`${
|
||||||
? "Tell AI what action to perform on this content..."
|
content && content !== ""
|
||||||
: "Ask AI anything..."
|
? "Tell AI what action to perform on this content..."
|
||||||
}`}
|
: "Ask AI anything..."
|
||||||
|
}`}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
||||||
@ -218,8 +219,8 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
{isSubmitting
|
{isSubmitting
|
||||||
? "Generating response..."
|
? "Generating response..."
|
||||||
: response === ""
|
: response === ""
|
||||||
? "Generate response"
|
? "Generate response"
|
||||||
: "Generate again"}
|
: "Generate again"}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,10 +18,9 @@ import type { ICurrentUserResponse, IIssueComment } from "types";
|
|||||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
||||||
|
|
||||||
const TiptapEditor = React.forwardRef<
|
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
||||||
ITiptapRichTextEditor,
|
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
|
||||||
ITiptapRichTextEditor
|
);
|
||||||
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
|
|
||||||
|
|
||||||
TiptapEditor.displayName = "TiptapEditor";
|
TiptapEditor.displayName = "TiptapEditor";
|
||||||
|
|
||||||
@ -88,15 +87,17 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="issue-comments-section">
|
<div id="tiptap-container" className="issue-comments-section">
|
||||||
<Controller
|
<Controller
|
||||||
name="comment_html"
|
name="comment_html"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) =>
|
render={({ field: { value, onChange } }) => (
|
||||||
<TiptapEditor
|
<TiptapEditor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={
|
value={
|
||||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
!value ||
|
||||||
|
value === "" ||
|
||||||
|
(typeof value === "object" && Object.keys(value).length === 0)
|
||||||
? watch("comment_html")
|
? watch("comment_html")
|
||||||
: value
|
: value
|
||||||
}
|
}
|
||||||
@ -107,7 +108,7 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
|
|||||||
setValue("comment_json", comment_json);
|
setValue("comment_json", comment_json);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SecondaryButton type="submit" disabled={isSubmitting || disabled} className="mt-2">
|
<SecondaryButton type="submit" disabled={isSubmitting || disabled} className="mt-2">
|
||||||
|
@ -15,10 +15,9 @@ import { timeAgo } from "helpers/date-time.helper";
|
|||||||
import type { IIssueComment } from "types";
|
import type { IIssueComment } from "types";
|
||||||
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
||||||
|
|
||||||
const TiptapEditor = React.forwardRef<
|
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
||||||
ITiptapRichTextEditor,
|
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
|
||||||
ITiptapRichTextEditor
|
);
|
||||||
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
|
|
||||||
|
|
||||||
TiptapEditor.displayName = "TiptapEditor";
|
TiptapEditor.displayName = "TiptapEditor";
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
|
|
||||||
onSubmit(formData);
|
onSubmit(formData);
|
||||||
console.log("watching", formData.comment_html)
|
console.log("watching", formData.comment_html);
|
||||||
|
|
||||||
editorRef.current?.setEditorValue(formData.comment_html);
|
editorRef.current?.setEditorValue(formData.comment_html);
|
||||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||||
@ -103,16 +102,18 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
|||||||
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
|
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
|
||||||
onSubmit={handleSubmit(onEnter)}
|
onSubmit={handleSubmit(onEnter)}
|
||||||
>
|
>
|
||||||
<TiptapEditor
|
<div id="tiptap-container">
|
||||||
ref={editorRef}
|
<TiptapEditor
|
||||||
value={watch("comment_html")}
|
ref={editorRef}
|
||||||
debouncedUpdatesEnabled={false}
|
value={watch("comment_html")}
|
||||||
customClassName="min-h-[50px] p-3"
|
debouncedUpdatesEnabled={false}
|
||||||
onChange={(comment_json: Object, comment_html: string) => {
|
customClassName="min-h-[50px] p-3"
|
||||||
setValue("comment_json", comment_json);
|
onChange={(comment_json: Object, comment_html: string) => {
|
||||||
setValue("comment_html", comment_html);
|
setValue("comment_json", comment_json);
|
||||||
}}
|
setValue("comment_html", comment_html);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex gap-1 self-end">
|
<div className="flex gap-1 self-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -110,7 +110,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span>{errors.name ? errors.name.message : null}</span>
|
<span>{errors.name ? errors.name.message : null}</span>
|
||||||
<div className="relative">
|
<div id="tiptap-container" className="relative">
|
||||||
<Controller
|
<Controller
|
||||||
name="description_html"
|
name="description_html"
|
||||||
control={control}
|
control={control}
|
||||||
|
@ -329,13 +329,14 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
|
||||||
<div className="relative">
|
<div id="tiptap-container" className="relative">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
{issueName && issueName !== "" && (
|
{issueName && issueName !== "" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${iAmFeelingLucky ? "cursor-wait" : ""
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
|
||||||
}`}
|
iAmFeelingLucky ? "cursor-wait" : ""
|
||||||
|
}`}
|
||||||
onClick={handleAutoGenerateDescription}
|
onClick={handleAutoGenerateDescription}
|
||||||
disabled={iAmFeelingLucky}
|
disabled={iAmFeelingLucky}
|
||||||
>
|
>
|
||||||
@ -367,7 +368,9 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
<Tiptap
|
<Tiptap
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
value={
|
value={
|
||||||
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
|
!value ||
|
||||||
|
value === "" ||
|
||||||
|
(typeof value === "object" && Object.keys(value).length === 0)
|
||||||
? watch("description_html")
|
? watch("description_html")
|
||||||
: value
|
: value
|
||||||
}
|
}
|
||||||
@ -545,7 +548,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||||
>
|
>
|
||||||
<span className="text-xs">Create more</span>
|
<span className="text-xs">Create more</span>
|
||||||
<ToggleSwitch value={createMore} onChange={() => { }} size="md" />
|
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
|
||||||
@ -555,8 +558,8 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
? "Updating Issue..."
|
? "Updating Issue..."
|
||||||
: "Update Issue"
|
: "Update Issue"
|
||||||
: isSubmitting
|
: isSubmitting
|
||||||
? "Adding Issue..."
|
? "Adding Issue..."
|
||||||
: "Add Issue"}
|
: "Add Issue"}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,10 +39,9 @@ const defaultValues = {
|
|||||||
description_html: null,
|
description_html: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TiptapEditor = React.forwardRef<
|
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
||||||
ITiptapRichTextEditor,
|
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
|
||||||
ITiptapRichTextEditor
|
);
|
||||||
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
|
|
||||||
|
|
||||||
TiptapEditor.displayName = "TiptapEditor";
|
TiptapEditor.displayName = "TiptapEditor";
|
||||||
|
|
||||||
@ -232,9 +231,9 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
description:
|
description:
|
||||||
!data.description || data.description === ""
|
!data.description || data.description === ""
|
||||||
? {
|
? {
|
||||||
type: "doc",
|
type: "doc",
|
||||||
content: [{ type: "paragraph" }],
|
content: [{ type: "paragraph" }],
|
||||||
}
|
}
|
||||||
: data.description,
|
: data.description,
|
||||||
description_html: data.description_html ?? "<p></p>",
|
description_html: data.description_html ?? "<p></p>",
|
||||||
});
|
});
|
||||||
@ -285,7 +284,10 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
maxLength={255}
|
maxLength={255}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-block-section relative -mt-2 text-custom-text-200">
|
<div
|
||||||
|
id="tiptap-container"
|
||||||
|
className="page-block-section relative -mt-2 text-custom-text-200"
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="description_html"
|
name="description_html"
|
||||||
control={control}
|
control={control}
|
||||||
@ -317,8 +319,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
value && value !== "" && Object.keys(value).length > 0
|
value && value !== "" && Object.keys(value).length > 0
|
||||||
? value
|
? value
|
||||||
: watch("description_html") && watch("description_html") !== ""
|
: watch("description_html") && watch("description_html") !== ""
|
||||||
? watch("description_html")
|
? watch("description_html")
|
||||||
: { type: "doc", content: [{ type: "paragraph" }] }
|
: { type: "doc", content: [{ type: "paragraph" }] }
|
||||||
}
|
}
|
||||||
debouncedUpdatesEnabled={false}
|
debouncedUpdatesEnabled={false}
|
||||||
customClassName="text-sm"
|
customClassName="text-sm"
|
||||||
@ -335,8 +337,9 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
<div className="m-2 mt-6 flex">
|
<div className="m-2 mt-6 flex">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
|
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||||
}`}
|
iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
|
||||||
|
}`}
|
||||||
onClick={handleAutoGenerateDescription}
|
onClick={handleAutoGenerateDescription}
|
||||||
disabled={iAmFeelingLucky}
|
disabled={iAmFeelingLucky}
|
||||||
>
|
>
|
||||||
@ -368,8 +371,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
|||||||
? "Updating..."
|
? "Updating..."
|
||||||
: "Update block"
|
: "Update block"
|
||||||
: isSubmitting
|
: isSubmitting
|
||||||
? "Adding..."
|
? "Adding..."
|
||||||
: "Add block"}
|
: "Add block"}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useEditor, EditorContent, Editor } from '@tiptap/react';
|
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { EditorBubbleMenu } from './bubble-menu';
|
import { EditorBubbleMenu } from "./bubble-menu";
|
||||||
import { TiptapExtensions } from './extensions';
|
import { TiptapExtensions } from "./extensions";
|
||||||
import { TiptapEditorProps } from './props';
|
import { TiptapEditorProps } from "./props";
|
||||||
import { Node } from "@tiptap/pm/model";
|
import { Node } from "@tiptap/pm/model";
|
||||||
import { Editor as CoreEditor } from "@tiptap/core";
|
import { Editor as CoreEditor } from "@tiptap/core";
|
||||||
import { useCallback, useImperativeHandle, useRef } from 'react';
|
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||||
import { EditorState } from '@tiptap/pm/state';
|
import { EditorState } from "@tiptap/pm/state";
|
||||||
import fileService from 'services/file.service';
|
import fileService from "services/file.service";
|
||||||
|
|
||||||
export interface ITiptapRichTextEditor {
|
export interface ITiptapRichTextEditor {
|
||||||
value: string;
|
value: string;
|
||||||
@ -34,7 +34,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
|||||||
value,
|
value,
|
||||||
noBorder,
|
noBorder,
|
||||||
borderOnFocus,
|
borderOnFocus,
|
||||||
customClassName
|
customClassName,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
@ -45,43 +45,40 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
|||||||
onUpdate: async ({ editor }) => {
|
onUpdate: async ({ editor }) => {
|
||||||
// for instant feedback loop
|
// for instant feedback loop
|
||||||
setIsSubmitting?.(true);
|
setIsSubmitting?.(true);
|
||||||
checkForNodeDeletions(editor)
|
checkForNodeDeletions(editor);
|
||||||
if (debouncedUpdatesEnabled) {
|
if (debouncedUpdatesEnabled) {
|
||||||
debouncedUpdates({ onChange, editor });
|
debouncedUpdates({ onChange, editor });
|
||||||
} else {
|
} else {
|
||||||
onChange?.(editor.getJSON(), editor.getHTML());
|
onChange?.(editor.getJSON(), editor.getHTML());
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const editorRef: React.MutableRefObject<Editor | null> = useRef(null)
|
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
|
||||||
|
|
||||||
useImperativeHandle(forwardedRef, () => ({
|
useImperativeHandle(forwardedRef, () => ({
|
||||||
clearEditor: () => {
|
clearEditor: () => {
|
||||||
console.log('clearContent')
|
console.log("clearContent");
|
||||||
console.log(editorRef)
|
console.log(editorRef);
|
||||||
editorRef.current?.commands.clearContent()
|
editorRef.current?.commands.clearContent();
|
||||||
},
|
},
|
||||||
setEditorValue: (content: string) => {
|
setEditorValue: (content: string) => {
|
||||||
console.log(editorRef, forwardedRef, content)
|
console.log(editorRef, forwardedRef, content);
|
||||||
editorRef.current?.commands.setContent(content)
|
editorRef.current?.commands.setContent(content);
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
const previousState = useRef<EditorState>();
|
const previousState = useRef<EditorState>();
|
||||||
|
|
||||||
const onNodeDeleted = useCallback(
|
const onNodeDeleted = useCallback(async (node: Node) => {
|
||||||
async (node: Node) => {
|
if (node.type.name === "image") {
|
||||||
if (node.type.name === 'image') {
|
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
|
||||||
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
|
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
||||||
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
if (resStatus === 204) {
|
||||||
if (resStatus === 204) {
|
console.log("file deleted successfully");
|
||||||
console.log("file deleted successfully");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[],
|
}, []);
|
||||||
);
|
|
||||||
|
|
||||||
const checkForNodeDeletions = useCallback(
|
const checkForNodeDeletions = useCallback(
|
||||||
(editor: CoreEditor) => {
|
(editor: CoreEditor) => {
|
||||||
@ -107,7 +104,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onNodeDeleted],
|
[onNodeDeleted]
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
|
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
|
||||||
@ -119,19 +116,19 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200
|
const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200
|
||||||
${noBorder ? '' : 'border border-custom-border-200'
|
${noBorder ? "" : "border border-custom-border-200"} ${
|
||||||
} ${borderOnFocus ? 'focus:border border-custom-border-200' : 'focus:border-0'
|
borderOnFocus ? "focus:border border-custom-border-200" : "focus:border-0"
|
||||||
} ${customClassName}`;
|
} ${customClassName}`;
|
||||||
|
|
||||||
if (!editor) return null
|
if (!editor) return null;
|
||||||
editorRef.current = editor
|
editorRef.current = editor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor?.chain().focus().run();
|
editor?.chain().focus().run();
|
||||||
}}
|
}}
|
||||||
className={`tiptap-editor-container relative ${editorClassNames}`}
|
className={`tiptap-editor-container cursor-text relative ${editorClassNames}`}
|
||||||
>
|
>
|
||||||
{editor && <EditorBubbleMenu editor={editor} />}
|
{editor && <EditorBubbleMenu editor={editor} />}
|
||||||
<div className={`${editorContentCustomClassNames}`}>
|
<div className={`${editorContentCustomClassNames}`}>
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
import React, {
|
import React, { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
ReactNode,
|
|
||||||
useRef,
|
|
||||||
useLayoutEffect,
|
|
||||||
} from "react";
|
|
||||||
import { Editor, Range, Extension } from "@tiptap/core";
|
import { Editor, Range, Extension } from "@tiptap/core";
|
||||||
import Suggestion from "@tiptap/suggestion";
|
import Suggestion from "@tiptap/suggestion";
|
||||||
import { ReactRenderer } from "@tiptap/react";
|
import { ReactRenderer } from "@tiptap/react";
|
||||||
@ -42,15 +35,7 @@ const Command = Extension.create({
|
|||||||
return {
|
return {
|
||||||
suggestion: {
|
suggestion: {
|
||||||
char: "/",
|
char: "/",
|
||||||
command: ({
|
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
||||||
editor,
|
|
||||||
range,
|
|
||||||
props,
|
|
||||||
}: {
|
|
||||||
editor: Editor;
|
|
||||||
range: Range;
|
|
||||||
props: any;
|
|
||||||
}) => {
|
|
||||||
props.command({ editor, range });
|
props.command({ editor, range });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -74,12 +59,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
|||||||
searchTerms: ["p", "paragraph"],
|
searchTerms: ["p", "paragraph"],
|
||||||
icon: <Text size={18} />,
|
icon: <Text size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.deleteRange(range)
|
|
||||||
.toggleNode("paragraph", "paragraph")
|
|
||||||
.run();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -88,12 +68,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
|||||||
searchTerms: ["title", "big", "large"],
|
searchTerms: ["title", "big", "large"],
|
||||||
icon: <Heading1 size={18} />,
|
icon: <Heading1 size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.deleteRange(range)
|
|
||||||
.setNode("heading", { level: 1 })
|
|
||||||
.run();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -102,12 +77,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
|||||||
searchTerms: ["subtitle", "medium"],
|
searchTerms: ["subtitle", "medium"],
|
||||||
icon: <Heading2 size={18} />,
|
icon: <Heading2 size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.deleteRange(range)
|
|
||||||
.setNode("heading", { level: 2 })
|
|
||||||
.run();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -116,12 +86,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
|||||||
searchTerms: ["subtitle", "small"],
|
searchTerms: ["subtitle", "small"],
|
||||||
icon: <Heading3 size={18} />,
|
icon: <Heading3 size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.deleteRange(range)
|
|
||||||
.setNode("heading", { level: 3 })
|
|
||||||
.run();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -148,7 +113,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
|||||||
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
|
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
|
||||||
icon: <MinusSquare size={18} />,
|
icon: <MinusSquare size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run()
|
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -209,12 +174,11 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
|||||||
return (
|
return (
|
||||||
item.title.toLowerCase().includes(search) ||
|
item.title.toLowerCase().includes(search) ||
|
||||||
item.description.toLowerCase().includes(search) ||
|
item.description.toLowerCase().includes(search) ||
|
||||||
(item.searchTerms &&
|
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
|
||||||
item.searchTerms.some((term: string) => term.includes(search)))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});;
|
});
|
||||||
|
|
||||||
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||||
const containerHeight = container.offsetHeight;
|
const containerHeight = container.offsetHeight;
|
||||||
@ -250,7 +214,7 @@ const CommandList = ({
|
|||||||
command(item);
|
command(item);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[command, items],
|
[command, items]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -297,12 +261,13 @@ const CommandList = ({
|
|||||||
<div
|
<div
|
||||||
id="slash-command"
|
id="slash-command"
|
||||||
ref={commandListContainer}
|
ref={commandListContainer}
|
||||||
className="z-50 h-auto max-h-[330px] w-72 overflow-y-auto rounded-md border border-custom-border-200 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
|
className="z-20 h-auto max-h-[330px] w-72 overflow-y-auto rounded-md border border-custom-border-200 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
|
||||||
>
|
>
|
||||||
{items.map((item: CommandItemProps, index: number) =>
|
{items.map((item: CommandItemProps, index: number) => (
|
||||||
<button
|
<button
|
||||||
className={`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-90 hover:text-custom-text-100 ${index === selectedIndex ? "bg-gray-800 text-custom-text-90" : ""
|
className={`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-90 hover:text-custom-text-100 ${
|
||||||
}`}
|
index === selectedIndex ? "bg-gray-800 text-custom-text-90" : ""
|
||||||
|
}`}
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => selectItem(index)}
|
onClick={() => selectItem(index)}
|
||||||
>
|
>
|
||||||
@ -311,7 +276,7 @@ const CommandList = ({
|
|||||||
<p className="text-xs text-stone-500">{item.description}</p>
|
<p className="text-xs text-stone-500">{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
@ -320,6 +285,8 @@ const renderItems = () => {
|
|||||||
let component: ReactRenderer | null = null;
|
let component: ReactRenderer | null = null;
|
||||||
let popup: any | null = null;
|
let popup: any | null = null;
|
||||||
|
|
||||||
|
const container = document.querySelector("#tiptap-container") as HTMLElement;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||||
component = new ReactRenderer(CommandList, {
|
component = new ReactRenderer(CommandList, {
|
||||||
@ -330,7 +297,7 @@ const renderItems = () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
popup = tippy("body", {
|
popup = tippy("body", {
|
||||||
getReferenceClientRect: props.clientRect,
|
getReferenceClientRect: props.clientRect,
|
||||||
appendTo: () => document.body,
|
appendTo: () => container,
|
||||||
content: component.element,
|
content: component.element,
|
||||||
showOnCreate: true,
|
showOnCreate: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
|
@ -629,17 +629,19 @@ const SinglePage: NextPage = () => {
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
>
|
>
|
||||||
{pageBlocks.map((block, index) => (
|
<>
|
||||||
<SinglePageBlock
|
{pageBlocks.map((block, index) => (
|
||||||
key={block.id}
|
<SinglePageBlock
|
||||||
block={block}
|
key={block.id}
|
||||||
projectDetails={projectDetails}
|
block={block}
|
||||||
showBlockDetails={showBlock}
|
projectDetails={projectDetails}
|
||||||
index={index}
|
showBlockDetails={showBlock}
|
||||||
user={user}
|
index={index}
|
||||||
/>
|
user={user}
|
||||||
))}
|
/>
|
||||||
{provided.placeholder}
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
|
@ -32,18 +32,18 @@
|
|||||||
|
|
||||||
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
|
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
|
||||||
|
|
||||||
ul[data-type="taskList"] li>label {
|
ul[data-type="taskList"] li > label {
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
ul[data-type="taskList"] li>label {
|
ul[data-type="taskList"] li > label {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul[data-type="taskList"] li>label input[type="checkbox"] {
|
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: rgb(var(--color-background-100));
|
background-color: rgb(var(--color-background-100));
|
||||||
@ -81,7 +81,7 @@ ul[data-type="taskList"] li>label input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul[data-type="taskList"] li[data-checked="true"]>div>p {
|
ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||||
color: rgb(var(--color-text-200));
|
color: rgb(var(--color-text-200));
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
|
Loading…
Reference in New Issue
Block a user