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,10 +179,11 @@ export const GptAssistantModal: React.FC<Props> = ({
type="text"
name="task"
register={register}
placeholder={`${content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
placeholder={`${
content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
autoComplete="off"
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
@ -218,8 +219,8 @@ export const GptAssistantModal: React.FC<Props> = ({
{isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again"}
? "Generate response"
: "Generate again"}
</PrimaryButton>
</div>
</div>

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,16 +102,18 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
onSubmit={handleSubmit(onEnter)}
>
<TiptapEditor
ref={editorRef}
value={watch("comment_html")}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3"
onChange={(comment_json: Object, comment_html: string) => {
setValue("comment_json", comment_json);
setValue("comment_html", comment_html);
}}
/>
<div id="tiptap-container">
<TiptapEditor
ref={editorRef}
value={watch("comment_html")}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3"
onChange={(comment_json: Object, comment_html: string) => {
setValue("comment_json", comment_json);
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,13 +329,14 @@ 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
}
@ -545,7 +548,7 @@ export const IssueForm: FC<IssueFormProps> = ({
onClick={() => setCreateMore((prevData) => !prevData)}
>
<span className="text-xs">Create more</span>
<ToggleSwitch value={createMore} onChange={() => { }} size="md" />
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
@ -555,8 +558,8 @@ export const IssueForm: FC<IssueFormProps> = ({
? "Updating Issue..."
: "Update Issue"
: isSubmitting
? "Adding Issue..."
: "Add Issue"}
? "Adding Issue..."
: "Add Issue"}
</PrimaryButton>
</div>
</div>

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";
@ -232,9 +231,9 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
description:
!data.description || data.description === ""
? {
type: "doc",
content: [{ type: "paragraph" }],
}
type: "doc",
content: [{ type: "paragraph" }],
}
: data.description,
description_html: data.description_html ?? "<p></p>",
});
@ -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}
@ -317,8 +319,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
value && value !== "" && Object.keys(value).length > 0
? value
: watch("description_html") && watch("description_html") !== ""
? watch("description_html")
: { type: "doc", content: [{ type: "paragraph" }] }
? watch("description_html")
: { type: "doc", content: [{ type: "paragraph" }] }
}
debouncedUpdatesEnabled={false}
customClassName="text-sm"
@ -335,8 +337,9 @@ 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}
>
@ -368,8 +371,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
? "Updating..."
: "Update block"
: isSubmitting
? "Adding..."
: "Add block"}
? "Adding..."
: "Add block"}
</PrimaryButton>
</div>
</form>

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 assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("file deleted successfully");
}
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'
} ${customClassName}`;
${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,12 +261,13 @@ 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,17 +629,19 @@ const SinglePage: NextPage = () => {
ref={provided.innerRef}
{...provided.droppableProps}
>
{pageBlocks.map((block, index) => (
<SinglePageBlock
key={block.id}
block={block}
projectDetails={projectDetails}
showBlockDetails={showBlock}
index={index}
user={user}
/>
))}
{provided.placeholder}
<>
{pageBlocks.map((block, index) => (
<SinglePageBlock
key={block.id}
block={block}
projectDetails={projectDetails}
showBlockDetails={showBlock}
index={index}
user={user}
/>
))}
{provided.placeholder}
</>
</div>
)}
</StrictModeDroppable>

View File

@ -32,18 +32,18 @@
/* 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;
user-select: none;
}
@media screen and (max-width: 768px) {
ul[data-type="taskList"] li>label {
ul[data-type="taskList"] li > label {
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;
appearance: none;
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));
text-decoration: line-through;
text-decoration-thickness: 2px;