mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: rich text editor (#1008)
* fix: undo/redo, placeholder overlapping with text, horizontal cursor refractor: removed a lot of state-management that was not required * fix: forwarding ref to remirror for getting extra helper functions * fix: icon type error * fix: value type not supported error on page block * style: spacing, and UX for add link --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
df96d40cfa
commit
4f2b106852
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, forwardRef, useRef } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import dynamic from "next/dynamic";
|
||||
@ -35,6 +35,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||
|
||||
const WrappedRemirrorRichTextEditor = forwardRef<IRemirrorRichTextEditor, IRemirrorRichTextEditor>(
|
||||
(props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />
|
||||
);
|
||||
|
||||
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||
|
||||
export const GptAssistantModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
@ -125,7 +133,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{((content && content !== "") || htmlContent !== "<p></p>") && (
|
||||
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
||||
<div className="remirror-section text-sm">
|
||||
Content:
|
||||
<RemirrorRichTextEditor
|
||||
|
@ -13,7 +13,7 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
// icons
|
||||
import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||
import { CogIcon, CloudUploadIcon, UsersIcon, CheckIcon } from "components/icons";
|
||||
import { CogIcon, UsersIcon, CheckIcon } from "components/icons";
|
||||
|
||||
// services
|
||||
import jiraImporterService from "services/integration/jira.service";
|
||||
@ -40,7 +40,7 @@ import { IJiraImporterForm } from "types";
|
||||
const integrationWorkflowData: Array<{
|
||||
title: string;
|
||||
key: TJiraIntegrationSteps;
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;
|
||||
}> = [
|
||||
{
|
||||
title: "Configure",
|
||||
|
@ -26,6 +26,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
||||
</Loader>
|
||||
),
|
||||
});
|
||||
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||
|
||||
const WrappedRemirrorRichTextEditor = React.forwardRef<
|
||||
IRemirrorRichTextEditor,
|
||||
IRemirrorRichTextEditor
|
||||
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
|
||||
|
||||
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||
|
||||
const defaultValues: Partial<IIssueComment> = {
|
||||
comment_json: "",
|
||||
@ -41,6 +49,8 @@ export const AddComment: React.FC = () => {
|
||||
reset,
|
||||
} = useForm<IIssueComment>({ defaultValues });
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
@ -61,6 +71,7 @@ export const AddComment: React.FC = () => {
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
@ -79,11 +90,12 @@ export const AddComment: React.FC = () => {
|
||||
name="comment_json"
|
||||
control={control}
|
||||
render={({ field: { value } }) => (
|
||||
<RemirrorRichTextEditor
|
||||
<WrappedRemirrorRichTextEditor
|
||||
value={value}
|
||||
onJSONChange={(jsonValue) => setValue("comment_json", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("comment_html", htmlValue)}
|
||||
placeholder="Enter your comment..."
|
||||
ref={editorRef}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -18,6 +18,15 @@ import type { IIssueComment } from "types";
|
||||
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false });
|
||||
|
||||
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||
|
||||
const WrappedRemirrorRichTextEditor = React.forwardRef<
|
||||
IRemirrorRichTextEditor,
|
||||
IRemirrorRichTextEditor
|
||||
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
|
||||
|
||||
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||
|
||||
type Props = {
|
||||
comment: IIssueComment;
|
||||
onSubmit: (comment: IIssueComment) => void;
|
||||
@ -27,6 +36,9 @@ type Props = {
|
||||
export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
|
||||
const { user } = useUser();
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const {
|
||||
@ -42,6 +54,10 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
if (isSubmitting) return;
|
||||
setIsEditing(false);
|
||||
onSubmit(formData);
|
||||
console.log(formData);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_json);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_json);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -85,41 +101,45 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
{isEditing ? (
|
||||
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onEnter)}>
|
||||
<RemirrorRichTextEditor
|
||||
value={comment.comment_html}
|
||||
onBlur={(jsonValue, htmlValue) => {
|
||||
setValue("comment_json", jsonValue);
|
||||
setValue("comment_html", htmlValue);
|
||||
}}
|
||||
placeholder="Enter Your comment..."
|
||||
/>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<RemirrorRichTextEditor
|
||||
<form
|
||||
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
|
||||
onSubmit={handleSubmit(onEnter)}
|
||||
>
|
||||
<WrappedRemirrorRichTextEditor
|
||||
value={comment.comment_html}
|
||||
onBlur={(jsonValue, htmlValue) => {
|
||||
setValue("comment_json", jsonValue);
|
||||
setValue("comment_html", htmlValue);
|
||||
}}
|
||||
placeholder="Enter Your comment..."
|
||||
ref={editorRef}
|
||||
/>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<CheckIcon className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<WrappedRemirrorRichTextEditor
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
noBorder
|
||||
customClassName="text-xs border border-brand-base bg-brand-base"
|
||||
ref={showEditorRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user?.id === comment.actor && (
|
||||
|
@ -6,6 +6,8 @@ import dynamic from "next/dynamic";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// contexts
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
// hooks
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
// components
|
||||
import { Loader, TextArea } from "components/ui";
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
@ -36,6 +38,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
||||
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
@ -82,7 +86,10 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
||||
useEffect(() => {
|
||||
if (!issue) return;
|
||||
|
||||
reset(issue);
|
||||
reset({
|
||||
...issue,
|
||||
description: issue.description,
|
||||
});
|
||||
}, [issue, reset]);
|
||||
|
||||
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
|
||||
@ -131,31 +138,42 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({ issue, handleFormS
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value } }) => (
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
!value ||
|
||||
value === "" ||
|
||||
(typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||
onBlur={() => {
|
||||
setIsSubmitting(true);
|
||||
handleSubmit(handleDescriptionFormSubmit)()
|
||||
.then(() => {
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
}}
|
||||
placeholder="Describe the issue..."
|
||||
editable={!isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
render={({ field: { value } }) => {
|
||||
if (!value || !watch("description_html")) return <></>;
|
||||
|
||||
return (
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
!value ||
|
||||
value === "" ||
|
||||
(typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
onJSONChange={(jsonValue) => {
|
||||
setShowAlert(true);
|
||||
setValue("description", jsonValue);
|
||||
}}
|
||||
onHTMLChange={(htmlValue) => {
|
||||
setShowAlert(true);
|
||||
setValue("description_html", htmlValue);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsSubmitting(true);
|
||||
handleSubmit(handleDescriptionFormSubmit)()
|
||||
.then(() => {
|
||||
setIsSubmitting(false);
|
||||
setShowAlert(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
}}
|
||||
placeholder="Describe the issue..."
|
||||
editable={!isNotAllowed}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`absolute -bottom-8 right-0 text-sm text-brand-secondary ${
|
||||
|
@ -53,7 +53,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
project: "",
|
||||
name: "",
|
||||
description: { type: "doc", content: [] },
|
||||
description: {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
},
|
||||
],
|
||||
},
|
||||
description_html: "<p></p>",
|
||||
estimate_point: null,
|
||||
state: "",
|
||||
@ -132,7 +139,14 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
reset({
|
||||
...defaultValues,
|
||||
project: projectId,
|
||||
description: "",
|
||||
description: {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
},
|
||||
],
|
||||
},
|
||||
description_html: "<p></p>",
|
||||
});
|
||||
};
|
||||
|
@ -34,8 +34,8 @@ type Props = {
|
||||
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
description: { type: "doc", content: [] },
|
||||
description_html: "<p></p>",
|
||||
description: null,
|
||||
description_html: null,
|
||||
};
|
||||
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
@ -46,6 +46,14 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
||||
</Loader>
|
||||
),
|
||||
});
|
||||
import { IRemirrorRichTextEditor } from "components/rich-text-editor";
|
||||
|
||||
const WrappedRemirrorRichTextEditor = React.forwardRef<
|
||||
IRemirrorRichTextEditor,
|
||||
IRemirrorRichTextEditor
|
||||
>((props, ref) => <RemirrorRichTextEditor {...props} forwardedRef={ref} />);
|
||||
|
||||
WrappedRemirrorRichTextEditor.displayName = "WrappedRemirrorRichTextEditor";
|
||||
|
||||
export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
handleClose,
|
||||
@ -57,6 +65,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||
|
||||
const editorRef = React.useRef<any>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, pageId } = router.query;
|
||||
|
||||
@ -97,6 +107,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
(prevData) => [...(prevData as IPageBlock[]), res],
|
||||
false
|
||||
);
|
||||
editorRef.current?.clearEditor();
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
@ -135,6 +146,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
})
|
||||
.then((res) => {
|
||||
mutate(PAGE_BLOCKS_LIST(pageId as string));
|
||||
editorRef.current?.setEditorValue(res.description);
|
||||
if (data.issue && data.sync)
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, data.issue, {
|
||||
@ -200,8 +212,14 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
reset({
|
||||
...defaultValues,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
description_html: data.description_html,
|
||||
description:
|
||||
!data.description || data.description === ""
|
||||
? {
|
||||
type: "doc",
|
||||
content: [{ type: "paragraph" }],
|
||||
}
|
||||
: data.description,
|
||||
description_html: data.description_html ?? "<p></p>",
|
||||
});
|
||||
}, [reset, data, focus, setFocus]);
|
||||
|
||||
@ -254,21 +272,43 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value } }) => (
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
!value || (typeof value === "object" && Object.keys(value).length === 0)
|
||||
? watch("description_html")
|
||||
: value
|
||||
}
|
||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||
placeholder="Write something..."
|
||||
customClassName="text-sm"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
/>
|
||||
)}
|
||||
render={({ field: { value } }) => {
|
||||
if (!data)
|
||||
return (
|
||||
<WrappedRemirrorRichTextEditor
|
||||
value={value}
|
||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||
placeholder="Write something..."
|
||||
customClassName="text-sm"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
ref={editorRef}
|
||||
/>
|
||||
);
|
||||
else if (!value || !watch("description_html"))
|
||||
return (
|
||||
<div className="h-32 w-full flex items-center justify-center text-brand-secondary text-sm" />
|
||||
);
|
||||
|
||||
return (
|
||||
<RemirrorRichTextEditor
|
||||
value={
|
||||
value && value !== "" && Object.keys(value).length > 0
|
||||
? value
|
||||
: watch("description_html") && watch("description_html") !== ""
|
||||
? watch("description_html")
|
||||
: { type: "doc", content: [{ type: "paragraph" }] }
|
||||
}
|
||||
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
|
||||
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
|
||||
placeholder="Write something..."
|
||||
customClassName="text-sm"
|
||||
noBorder
|
||||
borderOnFocus={false}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="m-2 mt-6 flex">
|
||||
<button
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useCallback, FC, useState, useEffect } from "react";
|
||||
import { useCallback, useState, useImperativeHandle } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { InvalidContentHandler } from "remirror";
|
||||
import {
|
||||
BoldExtension,
|
||||
@ -27,14 +28,14 @@ import {
|
||||
EditorComponent,
|
||||
OnChangeJSON,
|
||||
OnChangeHTML,
|
||||
FloatingToolbar,
|
||||
FloatingWrapper,
|
||||
} from "@remirror/react";
|
||||
import { TableExtension } from "@remirror/extension-react-tables";
|
||||
// tlds
|
||||
import tlds from "tlds";
|
||||
// services
|
||||
import fileService from "services/file.service";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// components
|
||||
import { CustomFloatingToolbar } from "./toolbar/float-tool-tip";
|
||||
import { MentionAutoComplete } from "./mention-autocomplete";
|
||||
@ -53,12 +54,10 @@ export interface IRemirrorRichTextEditor {
|
||||
gptOption?: boolean;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
forwardedRef?: any;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import { FloatingWrapper, FloatingToolbar } from "@remirror/react";
|
||||
|
||||
const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
const RemirrorRichTextEditor: React.FC<IRemirrorRichTextEditor> = (props) => {
|
||||
const {
|
||||
placeholder,
|
||||
mentions = [],
|
||||
@ -73,11 +72,10 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
gptOption = false,
|
||||
noBorder = false,
|
||||
borderOnFocus = true,
|
||||
forwardedRef,
|
||||
} = props;
|
||||
|
||||
const [imageLoader, setImageLoader] = useState(false);
|
||||
const [jsonValue, setJsonValue] = useState<any>();
|
||||
const [htmlValue, setHtmlValue] = useState<any>();
|
||||
const [disableToolbar, setDisableToolbar] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -91,15 +89,11 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
);
|
||||
|
||||
const uploadImageHandler = (value: any): any => {
|
||||
setImageLoader(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("asset", value[0].file);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
|
||||
setImageLoader(true);
|
||||
|
||||
return [
|
||||
() =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
@ -114,7 +108,6 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
width: "100%",
|
||||
src: imageUrl,
|
||||
});
|
||||
setImageLoader(false);
|
||||
}),
|
||||
];
|
||||
} catch {
|
||||
@ -136,15 +129,25 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
new CalloutExtension({ defaultType: "warn" }),
|
||||
new CodeBlockExtension(),
|
||||
new CodeExtension(),
|
||||
new PlaceholderExtension({ placeholder: placeholder || "Enter text..." }),
|
||||
new PlaceholderExtension({
|
||||
placeholder: placeholder || "Enter text...",
|
||||
emptyNodeClass: "empty-node",
|
||||
}),
|
||||
new HistoryExtension(),
|
||||
new LinkExtension({
|
||||
autoLink: true,
|
||||
autoLinkAllowedTLDs: tlds,
|
||||
selectTextOnClick: true,
|
||||
defaultTarget: "_blank",
|
||||
}),
|
||||
new ImageExtension({
|
||||
enableResizing: true,
|
||||
uploadHandler: uploadImageHandler,
|
||||
createPlaceholder() {
|
||||
const div = document.createElement("div");
|
||||
div.className = "w-full aspect-video bg-brand-surface-2 animate-pulse";
|
||||
return div;
|
||||
},
|
||||
}),
|
||||
new DropCursorExtension(),
|
||||
new StrikeExtension(),
|
||||
@ -156,38 +159,25 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
}),
|
||||
new TableExtension(),
|
||||
],
|
||||
content: !value || (typeof value === "object" && Object.keys(value).length === 0) ? "" : value,
|
||||
content: value,
|
||||
selection: "start",
|
||||
stringHandler: "html",
|
||||
onError,
|
||||
});
|
||||
|
||||
const updateState = useCallback(
|
||||
(value: any) => {
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
clearEditor: () => {
|
||||
manager.view.updateState(manager.createState({ content: "", selection: "start" }));
|
||||
},
|
||||
setEditorValue: (value: any) => {
|
||||
manager.view.updateState(
|
||||
manager.createState({
|
||||
content:
|
||||
!value || (typeof value === "object" && Object.keys(value).length === 0) ? "" : value,
|
||||
selection: value === "" ? "start" : manager.view.state.selection,
|
||||
content: value,
|
||||
selection: "end",
|
||||
})
|
||||
);
|
||||
},
|
||||
[manager]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateState(value);
|
||||
}, [updateState, value]);
|
||||
|
||||
const handleJSONChange = (json: any) => {
|
||||
setJsonValue(json);
|
||||
onJSONChange(json);
|
||||
};
|
||||
|
||||
const handleHTMLChange = (value: string) => {
|
||||
setHtmlValue(value);
|
||||
onHTMLChange(value);
|
||||
};
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@ -195,50 +185,51 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
manager={manager}
|
||||
initialContent={state}
|
||||
classNames={[
|
||||
`p-4 relative focus:outline-none rounded-md focus:border-brand-base ${
|
||||
`p-3 relative focus:outline-none rounded-md focus:border-brand-base ${
|
||||
noBorder ? "" : "border border-brand-base"
|
||||
} ${
|
||||
borderOnFocus ? "focus:border border-brand-base" : "focus:border-0"
|
||||
} ${customClassName}`,
|
||||
]}
|
||||
editable={editable}
|
||||
onBlur={() => {
|
||||
onBlur(jsonValue, htmlValue);
|
||||
onBlur={(event) => {
|
||||
const html = event.helpers.getHTML();
|
||||
const json = event.helpers.getJSON();
|
||||
|
||||
setDisableToolbar(true);
|
||||
|
||||
onBlur(json, html);
|
||||
}}
|
||||
onFocus={() => setDisableToolbar(false)}
|
||||
>
|
||||
{(!value || value === "" || value?.content?.[0]?.content === undefined) &&
|
||||
!(typeof value === "string" && value.includes("<")) &&
|
||||
placeholder && (
|
||||
<p className="pointer-events-none absolute top-4 left-4 text-sm text-brand-secondary">
|
||||
{placeholder}
|
||||
</p>
|
||||
)}
|
||||
<EditorComponent />
|
||||
<div className="prose prose-brand max-w-full prose-p:my-1">
|
||||
<EditorComponent />
|
||||
</div>
|
||||
|
||||
{imageLoader && (
|
||||
<div className="p-4">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editable && (
|
||||
{editable && !disableToolbar && (
|
||||
<FloatingWrapper
|
||||
positioner="always"
|
||||
floatingLabel="Custom Floating Toolbar"
|
||||
renderOutsideEditor
|
||||
floatingLabel="Custom Floating Toolbar"
|
||||
>
|
||||
<FloatingToolbar className="z-[9999] overflow-hidden rounded">
|
||||
<CustomFloatingToolbar gptOption={gptOption} editorState={state} />
|
||||
<FloatingToolbar className="z-50 overflow-hidden rounded">
|
||||
<CustomFloatingToolbar
|
||||
gptOption={gptOption}
|
||||
editorState={state}
|
||||
setDisableToolbar={setDisableToolbar}
|
||||
/>
|
||||
</FloatingToolbar>
|
||||
</FloatingWrapper>
|
||||
)}
|
||||
|
||||
<MentionAutoComplete mentions={mentions} tags={tags} />
|
||||
{<OnChangeJSON onChange={handleJSONChange} />}
|
||||
{<OnChangeHTML onChange={handleHTMLChange} />}
|
||||
{<OnChangeJSON onChange={onJSONChange} />}
|
||||
{<OnChangeHTML onChange={onHTMLChange} />}
|
||||
</Remirror>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RemirrorRichTextEditor.displayName = "RemirrorRichTextEditor";
|
||||
|
||||
export default RemirrorRichTextEditor;
|
||||
|
@ -1,3 +1,16 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
HTMLProps,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from "remirror/extensions";
|
||||
// buttons
|
||||
import {
|
||||
ToggleBoldButton,
|
||||
@ -9,60 +22,295 @@ import {
|
||||
ToggleCodeButton,
|
||||
ToggleHeadingButton,
|
||||
useActive,
|
||||
CommandButton,
|
||||
useAttrs,
|
||||
useChainedCommands,
|
||||
useCurrentSelection,
|
||||
useExtensionEvent,
|
||||
useUpdateReason,
|
||||
} from "@remirror/react";
|
||||
import { EditorState } from "remirror";
|
||||
|
||||
type Props = {
|
||||
gptOption?: boolean;
|
||||
editorState: Readonly<EditorState>;
|
||||
setDisableToolbar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const CustomFloatingToolbar: React.FC<Props> = ({ gptOption, editorState }) => {
|
||||
const active = useActive();
|
||||
const useLinkShortcut = () => {
|
||||
const [linkShortcut, setLinkShortcut] = useState<ShortcutHandlerProps | undefined>();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
useExtensionEvent(
|
||||
LinkExtension,
|
||||
"onShortcut",
|
||||
useCallback(
|
||||
(props) => {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
|
||||
return setLinkShortcut(props);
|
||||
},
|
||||
[isEditing]
|
||||
)
|
||||
);
|
||||
|
||||
return { linkShortcut, isEditing, setIsEditing };
|
||||
};
|
||||
|
||||
const useFloatingLinkState = () => {
|
||||
const chain = useChainedCommands();
|
||||
const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut();
|
||||
const { to, empty } = useCurrentSelection();
|
||||
|
||||
const url = (useAttrs().link()?.href as string) ?? "";
|
||||
const [href, setHref] = useState<string>(url);
|
||||
|
||||
// A positioner which only shows for links.
|
||||
const linkPositioner = useMemo(() => createMarkPositioner({ type: "link" }), []);
|
||||
|
||||
const onRemove = useCallback(() => chain.removeLink().focus().run(), [chain]);
|
||||
|
||||
const updateReason = useUpdateReason();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateReason.doc || updateReason.selection) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [isEditing, setIsEditing, updateReason.doc, updateReason.selection]);
|
||||
|
||||
useEffect(() => {
|
||||
setHref(url);
|
||||
}, [url]);
|
||||
|
||||
const submitHref = useCallback(() => {
|
||||
setIsEditing(false);
|
||||
const range = linkShortcut ?? undefined;
|
||||
|
||||
if (href === "") {
|
||||
chain.removeLink();
|
||||
} else {
|
||||
chain.updateLink({ href, auto: false }, range);
|
||||
}
|
||||
|
||||
chain.focus(range?.to ?? to).run();
|
||||
}, [setIsEditing, linkShortcut, chain, href, to]);
|
||||
|
||||
const cancelHref = useCallback(() => {
|
||||
setIsEditing(false);
|
||||
}, [setIsEditing]);
|
||||
|
||||
const clickEdit = useCallback(() => {
|
||||
if (empty) {
|
||||
chain.selectLink();
|
||||
}
|
||||
|
||||
setIsEditing(true);
|
||||
}, [chain, empty, setIsEditing]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
href,
|
||||
setHref,
|
||||
linkShortcut,
|
||||
linkPositioner,
|
||||
isEditing,
|
||||
setIsEditing,
|
||||
clickEdit,
|
||||
onRemove,
|
||||
submitHref,
|
||||
cancelHref,
|
||||
}),
|
||||
[
|
||||
href,
|
||||
linkShortcut,
|
||||
linkPositioner,
|
||||
isEditing,
|
||||
clickEdit,
|
||||
onRemove,
|
||||
submitHref,
|
||||
cancelHref,
|
||||
setIsEditing,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const DelayAutoFocusInput = ({
|
||||
autoFocus,
|
||||
setDisableToolbar,
|
||||
...rest
|
||||
}: HTMLProps<HTMLInputElement> & {
|
||||
setDisableToolbar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDisableToolbar(false);
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frame);
|
||||
};
|
||||
}, [autoFocus, setDisableToolbar]);
|
||||
|
||||
useEffect(() => {
|
||||
setDisableToolbar(false);
|
||||
}, [setDisableToolbar]);
|
||||
|
||||
return (
|
||||
<div className="z-[99999] flex items-center gap-y-2 divide-x divide-brand-base rounded border border-brand-base bg-brand-surface-2 p-1 px-0.5 shadow-md">
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<ToggleHeadingButton
|
||||
attrs={{
|
||||
level: 1,
|
||||
}}
|
||||
/>
|
||||
<ToggleHeadingButton
|
||||
attrs={{
|
||||
level: 2,
|
||||
}}
|
||||
/>
|
||||
<ToggleHeadingButton
|
||||
attrs={{
|
||||
level: 3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<ToggleBoldButton />
|
||||
<ToggleItalicButton />
|
||||
<ToggleUnderlineButton />
|
||||
<ToggleStrikeButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<ToggleOrderedListButton />
|
||||
<ToggleBulletListButton />
|
||||
</div>
|
||||
{gptOption && (
|
||||
<>
|
||||
<label htmlFor="link-input" className="text-sm">
|
||||
Add Link
|
||||
</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
{...rest}
|
||||
onKeyDown={(e) => {
|
||||
if (rest.onKeyDown) rest.onKeyDown(e);
|
||||
setDisableToolbar(false);
|
||||
}}
|
||||
className={`${rest.className} mt-1`}
|
||||
onFocus={() => {
|
||||
setDisableToolbar(false);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setDisableToolbar(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomFloatingToolbar: React.FC<Props> = ({
|
||||
gptOption,
|
||||
editorState,
|
||||
setDisableToolbar,
|
||||
}) => {
|
||||
const { isEditing, setIsEditing, clickEdit, onRemove, submitHref, href, setHref, cancelHref } =
|
||||
useFloatingLinkState();
|
||||
|
||||
const active = useActive();
|
||||
const activeLink = active.link();
|
||||
|
||||
const handleClickEdit = useCallback(() => {
|
||||
clickEdit();
|
||||
}, [clickEdit]);
|
||||
|
||||
return (
|
||||
<div className="z-[99999] flex flex-col items-center gap-y-2 divide-x divide-y divide-brand-base rounded border border-brand-base bg-brand-surface-2 p-1 px-0.5 shadow-md">
|
||||
<div className="flex items-center gap-y-2 divide-x divide-brand-base">
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded py-1 px-1.5 text-xs hover:bg-brand-surface-1"
|
||||
onClick={() => console.log(editorState.selection.$anchor.nodeBefore)}
|
||||
>
|
||||
AI
|
||||
</button>
|
||||
<ToggleHeadingButton
|
||||
attrs={{
|
||||
level: 1,
|
||||
}}
|
||||
/>
|
||||
<ToggleHeadingButton
|
||||
attrs={{
|
||||
level: 2,
|
||||
}}
|
||||
/>
|
||||
<ToggleHeadingButton
|
||||
attrs={{
|
||||
level: 3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<ToggleBoldButton />
|
||||
<ToggleItalicButton />
|
||||
<ToggleUnderlineButton />
|
||||
<ToggleStrikeButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<ToggleOrderedListButton />
|
||||
<ToggleBulletListButton />
|
||||
</div>
|
||||
{gptOption && (
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded py-1 px-1.5 text-xs hover:bg-brand-surface-1"
|
||||
onClick={() => console.log(editorState.selection.$anchor.nodeBefore)}
|
||||
>
|
||||
AI
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<ToggleCodeButton />
|
||||
</div>
|
||||
{activeLink ? (
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<CommandButton
|
||||
commandName="openLink"
|
||||
onSelect={() => {
|
||||
window.open(href, "_blank");
|
||||
}}
|
||||
icon="externalLinkFill"
|
||||
enabled
|
||||
/>
|
||||
<CommandButton
|
||||
commandName="updateLink"
|
||||
onSelect={handleClickEdit}
|
||||
icon="pencilLine"
|
||||
enabled
|
||||
/>
|
||||
<CommandButton commandName="removeLink" onSelect={onRemove} icon="linkUnlink" enabled />
|
||||
</div>
|
||||
) : (
|
||||
<CommandButton
|
||||
commandName="updateLink"
|
||||
onSelect={() => {
|
||||
if (isEditing) {
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
handleClickEdit();
|
||||
}
|
||||
}}
|
||||
icon="link"
|
||||
enabled
|
||||
active={isEditing}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="p-2 w-full">
|
||||
<DelayAutoFocusInput
|
||||
autoFocus
|
||||
placeholder="Paste your link here..."
|
||||
id="link-input"
|
||||
setDisableToolbar={setDisableToolbar}
|
||||
className="w-full px-2 py-0.5"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setHref(e.target.value)}
|
||||
value={href}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
const { code } = e;
|
||||
|
||||
if (code === "Enter") {
|
||||
submitHref();
|
||||
}
|
||||
|
||||
if (code === "Escape") {
|
||||
cancelHref();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-x-1 px-2">
|
||||
<ToggleCodeButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
28
apps/app/hooks/use-reload-confirmation.tsx
Normal file
28
apps/app/hooks/use-reload-confirmation.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
const useReloadConfirmations = (message?: string) => {
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
|
||||
const handleBeforeUnload = useCallback(
|
||||
(event: BeforeUnloadEvent) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
return message ?? "Are you sure you want to leave?";
|
||||
},
|
||||
[message]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAlert) {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, [handleBeforeUnload, showAlert]);
|
||||
|
||||
return { setShowAlert };
|
||||
};
|
||||
|
||||
export default useReloadConfirmations;
|
@ -19,6 +19,7 @@
|
||||
"@remirror/pm": "^2.0.3",
|
||||
"@remirror/react": "^2.0.24",
|
||||
"@sentry/nextjs": "^7.36.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/react-datepicker": "^4.8.0",
|
||||
"axios": "^1.1.3",
|
||||
@ -61,7 +62,6 @@
|
||||
"eslint-config-next": "12.2.2",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-tailwindcss": "^0.2.7",
|
||||
"tailwindcss": "^3.1.6",
|
||||
"tsconfig": "*",
|
||||
"typescript": "4.7.4"
|
||||
|
@ -1,205 +1,59 @@
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
.empty-node::after {
|
||||
content: attr(data-placeholder);
|
||||
color: #ccc;
|
||||
font-style: italic;
|
||||
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 15px;
|
||||
margin-left: 1px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
white-space: break-spaces;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection {
|
||||
background: transparent;
|
||||
}
|
||||
.ProseMirror-hideselection *::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
.ProseMirror-hideselection {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
outline: 2px solid #8cf;
|
||||
}
|
||||
|
||||
/* Make sure li selections wrap around markers */
|
||||
|
||||
li.ProseMirror-selectednode {
|
||||
-moz-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
border: 2px solid #8cf;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Protect against generic img rules */
|
||||
|
||||
img.ProseMirror-separator {
|
||||
display: inline !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.ProseMirror-textblock-dropdown {
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu {
|
||||
margin: 0 -4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
margin-right: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown,
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown {
|
||||
vertical-align: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-wrap {
|
||||
padding: 1px 0 1px 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: "";
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu,
|
||||
.ProseMirror-menu-submenu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
color: #666;
|
||||
border: 1px solid #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 15;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item {
|
||||
cursor: pointer;
|
||||
padding: 2px 8px 2px 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap {
|
||||
position: relative;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: "";
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu {
|
||||
display: none;
|
||||
min-width: 4em;
|
||||
left: 100%;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu,
|
||||
.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
position: relative;
|
||||
min-height: 1em;
|
||||
color: #666;
|
||||
padding: 1px 6px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-bottom: 1px solid silver;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
cursor: text;
|
||||
line-height: 1.2;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
appearance: textfield;
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
display: inline-block;
|
||||
line-height: 0.8;
|
||||
vertical-align: -2px; /* Compensate for padding */
|
||||
padding: 2px 8px;
|
||||
vertical-align: -2px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin: 0 3px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid transparent;
|
||||
transition: background 50ms ease-in-out;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
@ -210,149 +64,6 @@ img.ProseMirror-separator {
|
||||
.ProseMirror-icon span {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid black;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
/* Add space around the hr to make clicking it easier */
|
||||
|
||||
.ProseMirror-example-setup-style hr {
|
||||
padding: 2px 10px;
|
||||
border: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style hr:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: silver;
|
||||
line-height: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror ul,
|
||||
.ProseMirror ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 3px solid #eee;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
background: white;
|
||||
padding: 5px 10px 5px 15px;
|
||||
border: 1px solid silver;
|
||||
position: fixed;
|
||||
border-radius: 3px;
|
||||
z-index: 11;
|
||||
box-shadow: -0.5px 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.ProseMirror-prompt h5 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 100%;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"],
|
||||
.ProseMirror-prompt textarea {
|
||||
background: #eee;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"] {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
color: #666;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close:after {
|
||||
content: "✕";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror-invalid {
|
||||
background: #ffc;
|
||||
border: 1px solid #cc7;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-buttons {
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
#editor,
|
||||
.editor {
|
||||
background: white;
|
||||
color: black;
|
||||
background-clip: padding-box;
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
padding: 5px 0;
|
||||
margin-bottom: 23px;
|
||||
}
|
||||
|
||||
.ProseMirror p:first-child,
|
||||
.ProseMirror h1:first-child,
|
||||
.ProseMirror h2:first-child,
|
||||
.ProseMirror h3:first-child,
|
||||
.ProseMirror h4:first-child,
|
||||
.ProseMirror h5:first-child,
|
||||
.ProseMirror h6:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding: 8px 8px 4px 14px;
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.remirror-editor-wrapper .remirror-editor {
|
||||
min-height: 150px;
|
||||
@ -371,76 +82,16 @@ img.ProseMirror-separator {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* heading styling */
|
||||
.remirror-editor-wrapper h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.remirror-editor-wrapper h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.remirror-editor-wrapper h3 {
|
||||
font-size: 1.17rem;
|
||||
}
|
||||
/* end heading styling */
|
||||
|
||||
/* list styling */
|
||||
.remirror-editor-wrapper ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.remirror-editor-wrapper ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
/* end list styling */
|
||||
|
||||
/* table styling */
|
||||
.remirror-editor-wrapper table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.remirror-editor-wrapper table td {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
/* end table styling */
|
||||
|
||||
/* link styling */
|
||||
.remirror-floating-popover {
|
||||
z-index: 20 !important;
|
||||
}
|
||||
|
||||
.remirror-floating-popover input {
|
||||
font-size: 0.75rem;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid #a8a6a6;
|
||||
box-shadow: 1px 1px 5px #c0bebe;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.remirror-editor-wrapper a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* end link styling */
|
||||
|
||||
/* format buttons styling */
|
||||
.MuiButtonBase-root {
|
||||
border: none !important;
|
||||
border-radius: 0.25rem !important;
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root:hover {
|
||||
background-color: rgb(229 231 235);
|
||||
}
|
||||
|
||||
.MuiButtonBase-root svg {
|
||||
fill: #000 !important;
|
||||
fill: rgb(var(--color-text-base)) !important;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.Mui-selected {
|
||||
background-color: rgb(229 231 235) !important;
|
||||
.MuiButtonBase-root.Mui-selected, .MuiButtonBase-root:hover {
|
||||
background-color: rgb(var(--color-bg-base)) !important;
|
||||
}
|
||||
/* end format buttons styling */
|
||||
|
@ -6,6 +6,10 @@ function withOpacity(variableName) {
|
||||
};
|
||||
}
|
||||
|
||||
function convertToRGB(variableName) {
|
||||
return `rgb(var(${variableName}))`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
|
||||
@ -49,9 +53,32 @@ module.exports = {
|
||||
"100%": { right: "0" },
|
||||
},
|
||||
},
|
||||
typography: ({ theme }) => ({
|
||||
brand: {
|
||||
css: {
|
||||
"--tw-prose-body": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-p": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-headings": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-lead": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-links": `${convertToRGB("--color-accent")}`,
|
||||
"--tw-prose-bold": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-counters": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-bullets": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-hr": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-quotes": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-quote-borders": `${convertToRGB("--color-border")}`,
|
||||
"--tw-prose-code": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-pre-code": `${convertToRGB("--color-text-base")}`,
|
||||
"--tw-prose-pre-bg": `${convertToRGB("--color-bg-base")}`,
|
||||
"--tw-prose-th-borders": `${convertToRGB("--color-border")}`,
|
||||
"--tw-prose-td-borders": `${convertToRGB("--color-border")}`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user