fix: slash command scroll posiiton

This commit is contained in:
Aaryan Khandelwal 2023-08-14 18:30:46 +05:30
parent bcc1131ec1
commit 78bb3085f3
10 changed files with 137 additions and 162 deletions

View File

@ -32,10 +32,9 @@ type FormData = {
task: string;
};
const TiptapEditor = React.forwardRef<
ITiptapRichTextEditor,
ITiptapRichTextEditor
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
TiptapEditor.displayName = "TiptapEditor";
@ -141,11 +140,12 @@ export const GptAssistantModal: React.FC<Props> = ({
return (
<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>")) && (
<div className="remirror-section text-sm">
<div id="tiptap-container" className="remirror-section text-sm">
Content:
<TiptapEditor
value={htmlContent ?? `<p>${content}</p>`}
@ -179,7 +179,8 @@ export const GptAssistantModal: React.FC<Props> = ({
type="text"
name="task"
register={register}
placeholder={`${content && content !== ""
placeholder={`${
content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}

View File

@ -18,10 +18,9 @@ import type { ICurrentUserResponse, IIssueComment } from "types";
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
const TiptapEditor = React.forwardRef<
ITiptapRichTextEditor,
ITiptapRichTextEditor
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
TiptapEditor.displayName = "TiptapEditor";
@ -88,15 +87,17 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="issue-comments-section">
<div id="tiptap-container" className="issue-comments-section">
<Controller
name="comment_html"
control={control}
render={({ field: { value, onChange } }) =>
render={({ field: { value, onChange } }) => (
<TiptapEditor
ref={editorRef}
value={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("comment_html")
: value
}
@ -107,7 +108,7 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
setValue("comment_json", comment_json);
}}
/>
}
)}
/>
<SecondaryButton type="submit" disabled={isSubmitting || disabled} className="mt-2">

View File

@ -15,10 +15,9 @@ import { timeAgo } from "helpers/date-time.helper";
import type { IIssueComment } from "types";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
const TiptapEditor = React.forwardRef<
ITiptapRichTextEditor,
ITiptapRichTextEditor
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
TiptapEditor.displayName = "TiptapEditor";
@ -51,7 +50,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
setIsEditing(false);
onSubmit(formData);
console.log("watching", formData.comment_html)
console.log("watching", formData.comment_html);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
@ -103,6 +102,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
onSubmit={handleSubmit(onEnter)}
>
<div id="tiptap-container">
<TiptapEditor
ref={editorRef}
value={watch("comment_html")}
@ -113,6 +113,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
setValue("comment_html", comment_html);
}}
/>
</div>
<div className="flex gap-1 self-end">
<button
type="submit"

View File

@ -110,7 +110,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
)}
</div>
<span>{errors.name ? errors.name.message : null}</span>
<div className="relative">
<div id="tiptap-container" className="relative">
<Controller
name="description_html"
control={control}

View File

@ -329,12 +329,13 @@ export const IssueForm: FC<IssueFormProps> = ({
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
<div className="relative">
<div id="tiptap-container" className="relative">
<div className="flex justify-end">
{issueName && issueName !== "" && (
<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}
disabled={iAmFeelingLucky}
@ -367,7 +368,9 @@ export const IssueForm: FC<IssueFormProps> = ({
<Tiptap
debouncedUpdatesEnabled={false}
value={
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}

View File

@ -39,10 +39,9 @@ const defaultValues = {
description_html: null,
};
const TiptapEditor = React.forwardRef<
ITiptapRichTextEditor,
ITiptapRichTextEditor
>((props, ref) => <Tiptap {...props} forwardedRef={ref} />);
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
TiptapEditor.displayName = "TiptapEditor";
@ -285,7 +284,10 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
maxLength={255}
/>
</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
name="description_html"
control={control}
@ -335,7 +337,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
<div className="m-2 mt-6 flex">
<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}
disabled={iAmFeelingLucky}

View File

@ -1,14 +1,14 @@
// @ts-nocheck
import { useEditor, EditorContent, Editor } from '@tiptap/react';
import { useDebouncedCallback } from 'use-debounce';
import { EditorBubbleMenu } from './bubble-menu';
import { TiptapExtensions } from './extensions';
import { TiptapEditorProps } from './props';
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import { useDebouncedCallback } from "use-debounce";
import { EditorBubbleMenu } from "./bubble-menu";
import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props";
import { Node } from "@tiptap/pm/model";
import { Editor as CoreEditor } from "@tiptap/core";
import { useCallback, useImperativeHandle, useRef } from 'react';
import { EditorState } from '@tiptap/pm/state';
import fileService from 'services/file.service';
import { useCallback, useImperativeHandle, useRef } from "react";
import { EditorState } from "@tiptap/pm/state";
import fileService from "services/file.service";
export interface ITiptapRichTextEditor {
value: string;
@ -34,7 +34,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
value,
noBorder,
borderOnFocus,
customClassName
customClassName,
} = props;
const editor = useEditor({
@ -45,43 +45,40 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.(true);
checkForNodeDeletions(editor)
checkForNodeDeletions(editor);
if (debouncedUpdatesEnabled) {
debouncedUpdates({ onChange, editor });
} else {
onChange?.(editor.getJSON(), editor.getHTML());
}
}
},
});
const editorRef: React.MutableRefObject<Editor | null> = useRef(null)
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({
clearEditor: () => {
console.log('clearContent')
console.log(editorRef)
editorRef.current?.commands.clearContent()
console.log("clearContent");
console.log(editorRef);
editorRef.current?.commands.clearContent();
},
setEditorValue: (content: string) => {
console.log(editorRef, forwardedRef, content)
editorRef.current?.commands.setContent(content)
}
}))
console.log(editorRef, forwardedRef, content);
editorRef.current?.commands.setContent(content);
},
}));
const previousState = useRef<EditorState>();
const onNodeDeleted = useCallback(
async (node: Node) => {
if (node.type.name === 'image') {
const onNodeDeleted = useCallback(async (node: Node) => {
if (node.type.name === "image") {
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("file deleted successfully");
}
}
},
[],
);
}, []);
const checkForNodeDeletions = useCallback(
(editor: CoreEditor) => {
@ -107,7 +104,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
}
}
},
[onNodeDeleted],
[onNodeDeleted]
);
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
@ -119,19 +116,19 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
}, 1000);
const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200
${noBorder ? '' : 'border border-custom-border-200'
} ${borderOnFocus ? 'focus:border border-custom-border-200' : 'focus:border-0'
${noBorder ? "" : "border border-custom-border-200"} ${
borderOnFocus ? "focus:border border-custom-border-200" : "focus:border-0"
} ${customClassName}`;
if (!editor) return null
editorRef.current = editor
if (!editor) return null;
editorRef.current = editor;
return (
<div
onClick={() => {
editor?.chain().focus().run();
}}
className={`tiptap-editor-container relative ${editorClassNames}`}
className={`tiptap-editor-container cursor-text relative ${editorClassNames}`}
>
{editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}>

View File

@ -1,11 +1,4 @@
import React, {
useState,
useEffect,
useCallback,
ReactNode,
useRef,
useLayoutEffect,
} from "react";
import React, { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
import { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react";
@ -42,15 +35,7 @@ const Command = Extension.create({
return {
suggestion: {
char: "/",
command: ({
editor,
range,
props,
}: {
editor: Editor;
range: Range;
props: any;
}) => {
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
props.command({ editor, range });
},
},
@ -74,12 +59,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
searchTerms: ["p", "paragraph"],
icon: <Text size={18} />,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.run();
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
},
},
{
@ -88,12 +68,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
searchTerms: ["title", "big", "large"],
icon: <Heading1 size={18} />,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 1 })
.run();
editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
},
},
{
@ -102,12 +77,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
searchTerms: ["subtitle", "medium"],
icon: <Heading2 size={18} />,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 2 })
.run();
editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
},
},
{
@ -116,12 +86,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
searchTerms: ["subtitle", "small"],
icon: <Heading3 size={18} />,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 3 })
.run();
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
},
},
{
@ -148,7 +113,7 @@ const getSuggestionItems = ({ query }: { query: string }) =>
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
icon: <MinusSquare size={18} />,
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 (
item.title.toLowerCase().includes(search) ||
item.description.toLowerCase().includes(search) ||
(item.searchTerms &&
item.searchTerms.some((term: string) => term.includes(search)))
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
);
}
return true;
});;
});
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight;
@ -250,7 +214,7 @@ const CommandList = ({
command(item);
}
},
[command, items],
[command, items]
);
useEffect(() => {
@ -297,11 +261,12 @@ const CommandList = ({
<div
id="slash-command"
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
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}
onClick={() => selectItem(index)}
@ -311,7 +276,7 @@ const CommandList = ({
<p className="text-xs text-stone-500">{item.description}</p>
</div>
</button>
)}
))}
</div>
) : null;
};
@ -320,6 +285,8 @@ const renderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
const container = document.querySelector("#tiptap-container") as HTMLElement;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
component = new ReactRenderer(CommandList, {
@ -330,7 +297,7 @@ const renderItems = () => {
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
appendTo: () => container,
content: component.element,
showOnCreate: true,
interactive: true,

View File

@ -629,6 +629,7 @@ const SinglePage: NextPage = () => {
ref={provided.innerRef}
{...provided.droppableProps}
>
<>
{pageBlocks.map((block, index) => (
<SinglePageBlock
key={block.id}
@ -640,6 +641,7 @@ const SinglePage: NextPage = () => {
/>
))}
{provided.placeholder}
</>
</div>
)}
</StrictModeDroppable>