[WEB-1682] refactor: editor code splitting (#4893)

* refactor: lite and rich text editors

* refactor: document editor migration

* fix: add missing css import

* refactor: issue embed widget splitting

* chore: remove extensions folder from ee

* chore: update web ee folder structure

* fix: build errors

---------

Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2024-06-21 17:37:11 +05:30 committed by GitHub
parent 367ccba17e
commit dcdd1ef065
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
256 changed files with 1027 additions and 2401 deletions

View File

@ -7,7 +7,7 @@
"web",
"space",
"admin",
"packages/editor/*",
"packages/editor",
"packages/eslint-config-custom",
"packages/tailwind-config-custom",
"packages/tsconfig",

View File

@ -1,8 +1,8 @@
# @plane/editor-core
# @plane/editor
## Description
The `@plane/editor-core` package serves as the foundation for our editor system. It provides the base functionality for our other editor packages, but it will not be used directly in any of the projects but only for extending other editors.
The `@plane/editor` package serves as the foundation for our editor system. It provides the base functionality for our other editor packages, but it will not be used directly in any of the projects but only for extending other editors.
## Utilities
@ -64,40 +64,6 @@ const customEditorClassNames = getEditorClassNames({
- **Value Cleaning**: The Editors value is cleaned at the editor core level, eliminating the need for additional validation before sending from our app. This results in cleaner code and less potential for errors.
- **Turbo Pipeline**: Added a turbo pipeline for both dev and build tasks for projects depending on the editor package.
```json
"web#develop": {
"cache": false,
"persistent": true,
"dependsOn": [
"@plane/lite-text-editor#build",
"@plane/rich-text-editor#build"
]
},
"space#develop": {
"cache": false,
"persistent": true,
"dependsOn": [
"@plane/lite-text-editor#build",
"@plane/rich-text-editor#build"
]
},
"web#build": {
"cache": true,
"dependsOn": [
"@plane/lite-text-editor#build",
"@plane/rich-text-editor#build"
]
},
"space#build": {
"cache": true,
"dependsOn": [
"@plane/lite-text-editor#build",
"@plane/rich-text-editor#build"
]
},
```
## Base extensions included
- BulletList

View File

@ -1,34 +0,0 @@
// styles
// import "./styles/tailwind.css";
import "src/styles/editor.css";
import "src/styles/table.css";
import "src/styles/github-dark.css";
export { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell-selection";
// utils
export * from "src/lib/utils";
export * from "src/ui/extensions/table/table";
export { startImageUpload } from "src/ui/plugins/image/image-upload-handler";
// components
export { EditorContainer } from "src/ui/components/editor-container";
export { EditorContentWrapper } from "src/ui/components/editor-content";
// hooks
export { useEditor } from "src/hooks/use-editor";
export { useReadOnlyEditor } from "src/hooks/use-read-only-editor";
// helper items
export * from "src/ui/menus/menu-items";
export * from "src/lib/editor-commands";
// types
export type { CustomEditorProps, TFileHandler } from "src/hooks/use-editor";
export type { DeleteImage } from "src/types/delete-image";
export type { UploadImage } from "src/types/upload-image";
export type { EditorRefApi, EditorReadOnlyRefApi } from "src/types/editor-ref-api";
export type { RestoreImage } from "src/types/restore-image";
export type { IMentionHighlight, IMentionSuggestion } from "src/types/mention-suggestion";
export type { ISlashCommandItem, CommandProps } from "src/types/slash-commands-suggestion";
export type { LucideIconType } from "src/types/lucide-icon";

View File

@ -1 +0,0 @@
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;

View File

@ -1,19 +0,0 @@
import { IMarking } from "src/helpers/scroll-to-node";
import { EditorMenuItemNames } from "src/ui/menus/menu-items";
export type EditorReadOnlyRefApi = {
getMarkDown: () => string;
getHTML: () => string;
clearEditor: () => void;
setEditorValue: (content: string) => void;
scrollSummary: (marking: IMarking) => void;
};
export interface EditorRefApi extends EditorReadOnlyRefApi {
setEditorValueAtCursorPosition: (content: string) => void;
executeMenuItemCommand: (itemName: EditorMenuItemNames) => void;
isMenuItemActive: (itemName: EditorMenuItemNames) => boolean;
onStateChange: (callback: () => void) => () => void;
setFocusAtPosition: (position: number) => void;
isEditorReadyToDiscard: () => boolean;
}

View File

@ -1,3 +0,0 @@
import { Smile } from "lucide-react";
export type LucideIconType = typeof Smile;

View File

@ -1 +0,0 @@
export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<any>;

View File

@ -1 +0,0 @@
export type UploadImage = (file: File) => Promise<string>;

View File

@ -1,58 +0,0 @@
import { Mention, MentionOptions } from "@tiptap/extension-mention";
import { mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { MentionNodeView } from "src/ui/mentions/mention-node-view";
import { IMentionHighlight } from "src/types/mention-suggestion";
export interface CustomMentionOptions extends MentionOptions {
mentionHighlights: () => Promise<IMentionHighlight[]>;
readonly?: boolean;
}
export const CustomMention = Mention.extend<CustomMentionOptions>({
addStorage(this) {
return {
mentionsOpen: false,
};
},
addAttributes() {
return {
id: {
default: null,
},
label: {
default: null,
},
target: {
default: null,
},
self: {
default: false,
},
redirect_uri: {
default: "/",
},
entity_identifier: {
default: null,
},
entity_name: {
default: null,
},
};
},
addNodeView() {
return ReactNodeViewRenderer(MentionNodeView);
},
parseHTML() {
return [
{
tag: "mention-component",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["mention-component", mergeAttributes(HTMLAttributes)];
},
});

View File

@ -1,79 +0,0 @@
import { CustomMention } from "./custom";
import { ReactRenderer } from "@tiptap/react";
import { Editor } from "@tiptap/core";
import tippy from "tippy.js";
import { MentionList } from "./mention-list";
export const MentionsWithoutProps = () =>
CustomMention.configure({
HTMLAttributes: {
class: "mention",
},
// mentionHighlights: mentionHighlights,
suggestion: {
// @ts-expect-error - Tiptap types are incorrect
render: () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
if (!props.clientRect) {
return;
}
component = new ReactRenderer(MentionList, {
props: { ...props },
editor: props.editor,
});
props.editor.storage.mentionsOpen = true;
// @ts-expect-error - Tippy types are incorrect
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
component?.updateProps(props);
if (!props.clientRect) {
return;
}
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
return true;
}
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
if (navigationKeys.includes(props.event.key)) {
// @ts-expect-error - Tippy types are incorrect
component?.ref?.onKeyDown(props);
event?.stopPropagation();
return true;
}
return false;
},
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
props.editor.storage.mentionsOpen = false;
popup?.[0].destroy();
component?.destroy();
},
};
},
},
});

View File

@ -1,17 +0,0 @@
import { v4 as uuidv4 } from "uuid";
import { IMentionSuggestion } from "src/types/mention-suggestion";
export const getSuggestionItems =
(suggestions: IMentionSuggestion[]) =>
({ query }: { query: string }) => {
const mappedSuggestions: IMentionSuggestion[] = suggestions.map((suggestion): IMentionSuggestion => {
const transactionId = uuidv4();
return {
...suggestion,
id: transactionId,
};
});
return mappedSuggestions
.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
};

View File

@ -1,15 +0,0 @@
{
"extends": "tsconfig/react-library.json",
"include": [
"src/**/*",
"index.d.ts"
],
"exclude": [
"dist",
"build",
"node_modules"
],
"compilerOptions": {
"baseUrl": "."
}
}

View File

@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -1,6 +0,0 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -1,5 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1 +0,0 @@
# Document Editor

View File

@ -1,67 +0,0 @@
{
"name": "@plane/document-editor",
"version": "0.21.0",
"description": "Package that powers Plane's Pages Editor",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": [
"dist/**/*"
],
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"module": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsup --minify",
"dev": "tsup --watch",
"check-types": "tsc --noEmit",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"peerDependencies": {
"next": "12.3.2",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0"
},
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@plane/editor-core": "*",
"@plane/editor-extensions": "*",
"@plane/ui": "*",
"@tippyjs/react": "^4.2.6",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-collaboration": "^2.3.2",
"@tiptap/pm": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"lucide-react": "^0.378.0",
"react-popper": "^2.3.0",
"tippy.js": "^6.3.7",
"uuid": "^9.0.1",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.5",
"y-protocols": "^1.0.6",
"yjs": "^13.6.15"
},
"devDependencies": {
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
"typescript": "4.9.5"
},
"keywords": [
"editor",
"rich-text",
"markdown",
"nextjs",
"react"
]
}

View File

@ -1,9 +0,0 @@
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,11 +0,0 @@
export { DocumentEditor, DocumentEditorWithRef } from "src/ui";
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "src/ui/readonly";
// hooks
export { useEditorMarkings } from "src/hooks/use-editor-markings";
// utils
export { proseMirrorJSONToBinaryString, applyUpdates, mergeUpdates } from "src/utils/yjs";
export type { EditorRefApi, EditorReadOnlyRefApi, EditorMenuItem, EditorMenuItemNames } from "@plane/editor-core";
export type { IMarking } from "src/types/editor-types";

View File

@ -1,6 +0,0 @@
export interface IMarking {
type: "heading";
level: number;
text: string;
sequence: number;
}

View File

@ -1,13 +0,0 @@
export interface IDuplicationConfig {
action: () => Promise<void>;
}
export interface IPageLockConfig {
is_locked: boolean;
action: () => Promise<void>;
locked_by?: string;
}
export interface IPageArchiveConfig {
is_archived: boolean;
archived_at?: Date;
action: () => Promise<void>;
}

View File

@ -1 +0,0 @@
export * from "./page-renderer";

View File

@ -1,21 +0,0 @@
import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-widget";
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
import { UploadImage } from "@plane/editor-core";
import { CollaborationProvider } from "src/providers/collaboration-provider";
import Collaboration from "@tiptap/extension-collaboration";
type TArguments = {
uploadFile: UploadImage;
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
provider: CollaborationProvider;
};
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle, provider }: TArguments) => [
SlashCommand(uploadFile),
DragAndDrop(setHideDragHandle),
IssueWidgetPlaceholder(),
Collaboration.configure({
document: provider.document,
}),
];

View File

@ -1,54 +0,0 @@
import { Editor, Range } from "@tiptap/react";
import { IssueEmbedSuggestions } from "src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-extension";
import { getIssueSuggestionItems } from "src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-items";
import { IssueListRenderer } from "src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-renderer";
import { v4 as uuidv4 } from "uuid";
export type CommandProps = {
editor: Editor;
range: Range;
};
export interface IIssueListSuggestion {
title: string;
priority: "high" | "low" | "medium" | "urgent";
identifier: string;
state: "Cancelled" | "In Progress" | "Todo" | "Done" | "Backlog";
command: ({ editor, range }: CommandProps) => void;
}
export const IssueSuggestions = (suggestions: any[]) => {
const mappedSuggestions: IIssueListSuggestion[] = suggestions.map((suggestion): IIssueListSuggestion => {
const transactionId = uuidv4();
return {
title: suggestion.name,
priority: suggestion.priority.toString(),
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
state: suggestion.state_detail && suggestion.state_detail.name ? suggestion.state_detail.name : "Todo",
command: ({ editor, range }) => {
editor
.chain()
.focus()
.insertContentAt(range, {
type: "issue-embed-component",
attrs: {
entity_identifier: suggestion.id,
id: transactionId,
title: suggestion.name,
project_identifier: suggestion.project_detail.identifier,
sequence_id: suggestion.sequence_id,
entity_name: "issue",
},
})
.run();
},
};
});
return IssueEmbedSuggestions.configure({
suggestion: {
items: getIssueSuggestionItems(mappedSuggestions),
render: IssueListRenderer,
},
});
};

View File

@ -1,29 +0,0 @@
import { Extension, Range } from "@tiptap/core";
import { PluginKey } from "@tiptap/pm/state";
import { Editor } from "@tiptap/react";
import Suggestion from "@tiptap/suggestion";
export const IssueEmbedSuggestions = Extension.create({
name: "issue-embed-suggestions",
addOptions() {
return {
suggestion: {
char: "#issue_",
allowSpaces: true,
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
props.command({ editor, range });
},
},
};
},
addProseMirrorPlugins() {
return [
Suggestion({
pluginKey: new PluginKey("issue-embed-suggestions"),
editor: this.editor,
...this.options.suggestion,
}),
];
},
});

View File

@ -1,15 +0,0 @@
import { IIssueListSuggestion } from "src/ui/extensions/widgets/issue-embed-suggestion-list";
export const getIssueSuggestionItems =
(issueSuggestions: Array<IIssueListSuggestion>) =>
({ query }: { query: string }) => {
const search = query.toLowerCase();
const filteredSuggestions = issueSuggestions.filter(
(item) =>
item.title.toLowerCase().includes(search) ||
item.identifier.toLowerCase().includes(search) ||
item.priority.toLowerCase().includes(search)
);
return filteredSuggestions;
};

View File

@ -1,256 +0,0 @@
"use client";
import { cn } from "@plane/editor-core";
import { Editor } from "@tiptap/core";
import tippy from "tippy.js";
import { ReactRenderer } from "@tiptap/react";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { PriorityIcon } from "@plane/ui";
const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight;
const itemHeight = item ? item.offsetHeight : 0;
const top = item.offsetTop;
const bottom = top + itemHeight;
if (top < container.scrollTop) {
// container.scrollTop = top - containerHeight;
item.scrollIntoView({
behavior: "smooth",
block: "center",
});
} else if (bottom > containerHeight + container.scrollTop) {
// container.scrollTop = bottom - containerHeight;
item.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
};
interface IssueSuggestionProps {
title: string;
priority: "high" | "low" | "medium" | "urgent" | "none";
state: "Cancelled" | "In Progress" | "Todo" | "Done" | "Backlog";
identifier: string;
}
const IssueSuggestionList = ({
items,
command,
editor,
}: {
items: IssueSuggestionProps[];
command: any;
editor: Editor;
range: any;
}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const [currentSection, setCurrentSection] = useState<string>("Backlog");
const sections = ["Backlog", "In Progress", "Todo", "Done", "Cancelled"];
const [displayedItems, setDisplayedItems] = useState<{
[key: string]: IssueSuggestionProps[];
}>({});
const [displayedTotalLength, setDisplayedTotalLength] = useState(0);
const commandListContainer = useRef<HTMLDivElement>(null);
useEffect(() => {
const newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
let totalLength = 0;
sections.forEach((section) => {
newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
totalLength += newDisplayedItems[section].length;
});
setDisplayedTotalLength(totalLength);
setDisplayedItems(newDisplayedItems);
}, [items]);
const selectItem = useCallback(
(section: string, index: number) => {
const item = displayedItems[section][index];
if (item) {
command(item);
}
},
[command, displayedItems, currentSection]
);
useEffect(() => {
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"];
const onKeyDown = (e: KeyboardEvent) => {
if (navigationKeys.includes(e.key)) {
// if (editor.isFocused) {
// editor.chain().blur();
// commandListContainer.current?.focus();
// }
if (e.key === "ArrowUp") {
setSelectedIndex(
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
);
return true;
}
if (e.key === "ArrowDown") {
const nextIndex = (selectedIndex + 1) % displayedItems[currentSection].length;
setSelectedIndex(nextIndex);
if (nextIndex === 4) {
const nextItems = items
.filter((item) => item.state === currentSection)
.slice(displayedItems[currentSection].length, displayedItems[currentSection].length + 5);
setDisplayedItems((prevItems) => ({
...prevItems,
[currentSection]: [...prevItems[currentSection], ...nextItems],
}));
}
return true;
}
if (e.key === "Enter") {
selectItem(currentSection, selectedIndex);
return true;
}
if (e.key === "Tab") {
const currentSectionIndex = sections.indexOf(currentSection);
const nextSectionIndex = (currentSectionIndex + 1) % sections.length;
setCurrentSection(sections[nextSectionIndex]);
setSelectedIndex(0);
return true;
}
return false;
} else if (e.key === "Escape") {
if (!editor.isFocused) {
editor.chain().focus();
}
}
};
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [displayedItems, selectedIndex, setSelectedIndex, selectItem, currentSection]);
useLayoutEffect(() => {
const container = commandListContainer?.current;
if (container) {
const sectionContainer = container?.querySelector(`#${currentSection}-container`) as HTMLDivElement;
if (sectionContainer) {
updateScrollView(container, sectionContainer);
}
const sectionScrollContainer = container?.querySelector(`#${currentSection}`) as HTMLElement;
const item = sectionScrollContainer?.children[selectedIndex] as HTMLElement;
if (item && sectionScrollContainer) {
updateScrollView(sectionScrollContainer, item);
}
}
}, [selectedIndex, currentSection]);
return displayedTotalLength > 0 ? (
<div
id="issue-list-container"
ref={commandListContainer}
className=" fixed z-[10] max-h-80 w-96 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
>
{sections.map((section) => {
const sectionItems = displayedItems[section];
return (
sectionItems &&
sectionItems.length > 0 && (
<div className={"flex h-full w-full flex-col"} key={`${section}-container`} id={`${section}-container`}>
<h6
className={
"sticky top-0 z-[10] bg-custom-background-100 px-2 py-1 text-xs font-medium text-custom-text-400"
}
>
{section}
</h6>
<div key={section} id={section} className={"max-h-[140px] overflow-x-hidden overflow-y-scroll"}>
{sectionItems.map((item: IssueSuggestionProps, index: number) => (
<button
className={cn(
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
{
"bg-custom-primary-100/5 text-custom-text-100":
section === currentSection && index === selectedIndex,
}
)}
key={item.identifier}
onClick={() => selectItem(section, index)}
>
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
<PriorityIcon priority={item.priority} />
<div className="w-full truncate">
<p className="flex-grow w-full truncate text-xs">{item.title}</p>
</div>
</button>
))}
</div>
</div>
)
);
})}
</div>
) : null;
};
export const IssueListRenderer = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
const container = document.querySelector(".frame-renderer") as HTMLElement;
component = new ReactRenderer(IssueSuggestionList, {
props,
// @ts-ignore
editor: props.editor,
});
// @ts-ignore
popup = tippy(".frame-renderer", {
flipbehavior: ["bottom", "top"],
appendTo: () => document.querySelector(".frame-renderer") as HTMLElement,
flip: true,
flipOnUpdate: true,
getReferenceClientRect: props.clientRect,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
container.addEventListener("scroll", () => {
popup?.[0].destroy();
});
},
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
component?.updateProps(props);
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
return true;
}
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"];
if (navigationKeys.includes(props.event.key)) {
// @ts-ignore
component?.ref?.onKeyDown(props);
return true;
}
return false;
},
onExit: (e) => {
const container = document.querySelector(".frame-renderer") as HTMLElement;
if (container) {
container.removeEventListener("scroll", () => {});
}
popup?.[0].destroy();
setTimeout(() => {
component?.destroy();
}, 300);
},
};
};

View File

@ -1,3 +0,0 @@
import { IssueWidget } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-node";
export const IssueWidgetPlaceholder = () => IssueWidget.configure({});

View File

@ -1,35 +0,0 @@
"use client";
// @ts-nocheck
import { Button } from "@plane/ui";
import { NodeViewWrapper } from "@tiptap/react";
import { Crown } from "lucide-react";
export const IssueWidgetCard = (props) => (
<NodeViewWrapper className="issue-embed-component m-2">
<div
className={`${
props.selected ? "border-custom-primary-200 border-[2px]" : ""
} w-full h-[100px] cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs`}
>
<h5 className="h-[20%] text-xs text-custom-text-300 p-2">
{props.node.attrs.project_identifier}-{props.node.attrs.sequence_id}
</h5>
<div className="relative h-[71%]">
<div className="h-full backdrop-filter backdrop-blur-[30px] bg-custom-background-80 bg-opacity-30 flex items-center w-full justify-between gap-5 mt-2.5 pl-4 pr-5 py-3 max-md:max-w-full max-md:flex-wrap relative">
<div className="flex gap-2 items-center">
<div className="rounded">
<Crown className="m-2" size={16} color="#FFBA18" />
</div>
<div className="text-custom-text text-sm">
Embed and access issues in pages seamlessly, upgrade to plane pro now.
</div>
</div>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<Button>Upgrade</Button>
</a>
</div>
</div>
</div>
</NodeViewWrapper>
);

View File

@ -1,63 +0,0 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { IssueWidgetCard } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-card";
import { ReactNodeViewRenderer } from "@tiptap/react";
export const IssueWidget = Node.create({
name: "issue-embed-component",
group: "block",
atom: true,
addAttributes() {
return {
id: {
default: null,
},
class: {
default: "w-[600px]",
},
title: {
default: null,
},
entity_name: {
default: null,
},
entity_identifier: {
default: null,
},
project_identifier: {
default: null,
},
sequence_id: {
default: null,
},
};
},
addNodeView() {
return ReactNodeViewRenderer((props: Object) => <IssueWidgetCard {...props} />);
},
parseHTML() {
return [
{
tag: "issue-embed-component",
getAttrs: (node: string | HTMLElement) => {
if (typeof node === "string") {
return null;
}
return {
id: node.getAttribute("id") || "",
title: node.getAttribute("title") || "",
entity_name: node.getAttribute("entity_name") || "",
entity_identifier: node.getAttribute("entity_identifier") || "",
project_identifier: node.getAttribute("project_identifier") || "",
sequence_id: node.getAttribute("sequence_id") || "",
};
},
},
];
},
renderHTML({ HTMLAttributes }) {
return ["issue-embed-component", mergeAttributes(HTMLAttributes)];
},
});

View File

@ -1,10 +0,0 @@
import React from "react";
type Props = {
iconName: string;
className?: string;
};
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
<span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>
);

View File

@ -1,75 +0,0 @@
import * as React from "react";
// next-themes
import { useTheme } from "next-themes";
// tooltip2
import { Tooltip2 } from "@blueprintjs/popover2";
type Props = {
tooltipHeading?: string;
tooltipContent: string | React.ReactNode;
position?:
| "top"
| "right"
| "bottom"
| "left"
| "auto"
| "auto-end"
| "auto-start"
| "bottom-left"
| "bottom-right"
| "left-bottom"
| "left-top"
| "right-bottom"
| "right-top"
| "top-left"
| "top-right";
children: JSX.Element;
disabled?: boolean;
className?: string;
openDelay?: number;
closeDelay?: number;
};
export const Tooltip: React.FC<Props> = ({
tooltipHeading,
tooltipContent,
position = "top",
children,
disabled = false,
className = "",
openDelay = 200,
closeDelay,
}) => {
const { theme } = useTheme();
return (
<Tooltip2
disabled={disabled}
hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay}
content={
<div
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
} overflow-hidden break-words ${className}`}
>
{tooltipHeading && (
<h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
{tooltipHeading}
</h5>
)}
{tooltipContent}
</div>
}
position={position}
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
React.cloneElement(children, {
ref: eleReference,
...tooltipProps,
...children.props,
})
}
/>
);
};

View File

@ -1,26 +0,0 @@
function isNumber(value: any) {
return typeof value === "number";
}
/**
* This method returns a date from string of type yyyy-mm-dd
* This method is recommended to use instead of new Date() as this does not introduce any timezone offsets
* @param date
* @returns date or undefined
*/
export const getDate = (date: string | Date | undefined | null): Date | undefined => {
try {
if (!date || date === "") return;
if (typeof date !== "string" && !(date instanceof String)) return date;
const [yearString, monthString, dayString] = date.substring(0, 10).split("-");
const year = parseInt(yearString);
const month = parseInt(monthString);
const day = parseInt(dayString);
if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return;
return new Date(year, month - 1, day);
} catch (e) {
return undefined;
}
};

View File

@ -1,34 +0,0 @@
import { Editor } from "@tiptap/react";
import { IMarking } from "src/types/editor-types";
function findNthH1(editor: Editor, n: number, level: number): number {
let count = 0;
let pos = 0;
editor.state.doc.descendants((node, position) => {
if (node.type.name === "heading" && node.attrs.level === level) {
count++;
if (count === n) {
pos = position;
return false;
}
}
});
return pos;
}
function scrollToNode(editor: Editor, pos: number): void {
const headingNode = editor.state.doc.nodeAt(pos);
if (headingNode) {
const headingDOM = editor.view.nodeDOM(pos);
if (headingDOM instanceof HTMLElement) {
headingDOM.scrollIntoView({ behavior: "smooth" });
}
}
}
export function scrollSummary(editor: Editor, marking: IMarking) {
if (editor) {
const pos = findNthH1(editor, marking.sequence, marking.level);
scrollToNode(editor, pos);
}
}

View File

@ -1,6 +0,0 @@
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
module.exports = {
// prefix ui lib classes to avoid conflicting with the app
...sharedConfig,
};

View File

@ -1,15 +0,0 @@
{
"extends": "tsconfig/react-library.json",
"include": [
"src/**/*",
"index.d.ts"
],
"exclude": [
"dist",
"build",
"node_modules"
],
"compilerOptions": {
"baseUrl": "."
}
}

View File

@ -1,11 +0,0 @@
import { defineConfig, Options } from "tsup";
export default defineConfig((options: Options) => ({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
clean: false,
external: ["react"],
injectStyle: true,
...options,
}));

View File

@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -1,6 +0,0 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -1,5 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1,97 +0,0 @@
# @plane/editor-extensions
## Description
The `@plane/lite-text-editor` package extends from the `editor-core` package, inheriting its base functionality while adding its own unique features of Custom control over Enter key, etc.
## Key Features
- **Exported Components**: There are two components exported from the Lite text editor (with and without Ref), you can choose to use the `withRef` instance whenever you want to control the Editors state via a side effect of some external action from within the application code.
`LiteTextEditor` & `LiteTextEditorWithRef`
- **Read Only Editor Instances**: We have added a really light weight _Read Only_ Editor instance for the Lite editor types (with and without Ref)
`LiteReadOnlyEditor` &`LiteReadOnlyEditorWithRef`
## LiteTextEditor
| Prop | Type | Description |
| ------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uploadFile` | `(file: File) => Promise<string>` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. |
| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise<any>` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. |
| `value` | `html string` | The initial content of the editor. |
| `onEnterKeyPress` | `(e) => void` | The event that happens on Enter key press |
| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. |
| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. |
| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. |
| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". |
| `noBorder` | `boolean` | If set to true, the editor will not have a border. |
| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. |
| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. |
| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. |
### Usage
1. Here is an example of how to use the `RichTextEditor` component
```tsx
<LiteTextEditor
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
value={value}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
}}
/>
```
2. Example of how to use the `LiteTextEditorWithRef` component
```tsx
const editorRef = useRef<any>(null);
// can use it to set the editor's value
editorRef.current?.setEditorValue(`${watch("description_html")}`);
// can use it to clear the editor
editorRef?.current?.clearEditor();
return (
<LiteTextEditorWithRef
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
ref={editorRef}
value={value}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
}}
/>
);
```
## LiteReadOnlyEditor
| Prop | Type | Description |
| ------------------------------- | ------------- | --------------------------------------------------------------------- |
| `value` | `html string` | The initial content of the editor. |
| `noBorder` | `boolean` | If set to true, the editor will not have a border. |
| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. |
| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. |
| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. |
### Usage
Here is an example of how to use the `RichReadOnlyEditor` component
```tsx
<LiteReadOnlyEditor
value={comment.comment_html}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/>
```

View File

@ -1,58 +0,0 @@
{
"name": "@plane/editor-extensions",
"version": "0.21.0",
"description": "Package that powers Plane's Editor with extensions",
"private": true,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": [
"dist/**/*"
],
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"module": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsup --minify",
"dev": "tsup --watch",
"check-types": "tsc --noEmit"
},
"peerDependencies": {
"next": "12.3.2",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0"
},
"dependencies": {
"@plane/editor-core": "*",
"@plane/ui": "*",
"@tiptap/core": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/react": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"lucide-react": "^0.378.0",
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
"typescript": "4.9.5"
},
"keywords": [
"editor",
"rich-text",
"markdown",
"nextjs",
"react"
]
}

View File

@ -1,9 +0,0 @@
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,2 +0,0 @@
export * from "./drag-drop";
export * from "./slash-commands";

View File

@ -1,3 +0,0 @@
import "src/styles/drag-drop.css";
export { DragAndDrop, SlashCommand } from "src/extensions";

View File

@ -1,6 +0,0 @@
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
module.exports = {
// prefix ui lib classes to avoid conflicting with the app
...sharedConfig,
};

View File

@ -1,15 +0,0 @@
{
"extends": "tsconfig/react-library.json",
"include": [
"src/**/*",
"index.d.ts"
],
"exclude": [
"dist",
"build",
"node_modules"
],
"compilerOptions": {
"baseUrl": "."
}
}

View File

@ -1,11 +0,0 @@
import { defineConfig, Options } from "tsup";
export default defineConfig((options: Options) => ({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
clean: false,
external: ["react"],
injectStyle: true,
...options,
}));

View File

@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -1,6 +0,0 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -1,5 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1,97 +0,0 @@
# @plane/lite-text-editor
## Description
The `@plane/lite-text-editor` package extends from the `editor-core` package, inheriting its base functionality while adding its own unique features of Custom control over Enter key, etc.
## Key Features
- **Exported Components**: There are two components exported from the Lite text editor (with and without Ref), you can choose to use the `withRef` instance whenever you want to control the Editors state via a side effect of some external action from within the application code.
`LiteTextEditor` & `LiteTextEditorWithRef`
- **Read Only Editor Instances**: We have added a really light weight _Read Only_ Editor instance for the Lite editor types (with and without Ref)
`LiteReadOnlyEditor` &`LiteReadOnlyEditorWithRef`
## LiteTextEditor
| Prop | Type | Description |
| ------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uploadFile` | `(file: File) => Promise<string>` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. |
| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise<any>` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. |
| `value` | `html string` | The initial content of the editor. |
| `onEnterKeyPress` | `(e) => void` | The event that happens on Enter key press |
| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. |
| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. |
| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. |
| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". |
| `noBorder` | `boolean` | If set to true, the editor will not have a border. |
| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. |
| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. |
| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. |
### Usage
1. Here is an example of how to use the `RichTextEditor` component
```tsx
<LiteTextEditor
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
value={value}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
}}
/>
```
2. Example of how to use the `LiteTextEditorWithRef` component
```tsx
const editorRef = useRef<any>(null);
// can use it to set the editor's value
editorRef.current?.setEditorValue(`${watch("description_html")}`);
// can use it to clear the editor
editorRef?.current?.clearEditor();
return (
<LiteTextEditorWithRef
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
ref={editorRef}
value={value}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
}}
/>
);
```
## LiteReadOnlyEditor
| Prop | Type | Description |
| ------------------------------- | ------------- | --------------------------------------------------------------------- |
| `value` | `html string` | The initial content of the editor. |
| `noBorder` | `boolean` | If set to true, the editor will not have a border. |
| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. |
| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. |
| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. |
### Usage
Here is an example of how to use the `RichReadOnlyEditor` component
```tsx
<LiteReadOnlyEditor
value={comment.comment_html}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/>
```

View File

@ -1,53 +0,0 @@
{
"name": "@plane/lite-text-editor",
"version": "0.21.0",
"description": "Package that powers Plane's Comment Editor",
"private": true,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": [
"dist/**/*"
],
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"module": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsup --minify",
"dev": "tsup --watch",
"check-types": "tsc --noEmit",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"peerDependencies": {
"next": "12.3.2",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0"
},
"dependencies": {
"@plane/editor-core": "*",
"@plane/ui": "*"
},
"devDependencies": {
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.38",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
"typescript": "4.9.5"
},
"keywords": [
"editor",
"rich-text",
"markdown",
"nextjs",
"react"
]
}

View File

@ -1,9 +0,0 @@
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,7 +0,0 @@
export { LiteTextEditor, LiteTextEditorWithRef } from "src/ui";
export { LiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef } from "src/ui/read-only";
export type { IMentionSuggestion, IMentionHighlight } from "@plane/editor-core";
export type { ILiteTextEditor } from "src/ui";
export type { ILiteTextReadOnlyEditor } from "src/ui/read-only";
export type { EditorRefApi, EditorReadOnlyRefApi, EditorMenuItem, EditorMenuItemNames } from "@plane/editor-core";

View File

@ -1,3 +0,0 @@
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)];

View File

@ -1,87 +0,0 @@
import * as React from "react";
// editor-core
import {
IMentionSuggestion,
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
useEditor,
IMentionHighlight,
EditorRefApi,
TFileHandler,
} from "@plane/editor-core";
// extensions
import { LiteTextEditorExtensions } from "src/ui/extensions";
export interface ILiteTextEditor {
initialValue: string;
value?: string | null;
fileHandler: TFileHandler;
containerClassName?: string;
editorClassName?: string;
onChange?: (json: object, html: string) => void;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
onEnterKeyPress?: (e?: any) => void;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
suggestions?: () => Promise<IMentionSuggestion[]>;
};
tabIndex?: number;
placeholder?: string | ((isFocused: boolean, value: string) => string);
id?: string;
}
const LiteTextEditor = (props: ILiteTextEditor) => {
const {
onChange,
initialValue,
fileHandler,
value,
containerClassName,
editorClassName = "",
forwardedRef,
onEnterKeyPress,
tabIndex,
mentionHandler,
placeholder = "Add comment...",
id = "",
} = props;
const editor = useEditor({
onChange,
initialValue,
value,
id,
editorClassName,
fileHandler,
forwardedRef,
extensions: LiteTextEditorExtensions(onEnterKeyPress),
mentionHandler,
placeholder,
tabIndex,
});
const editorContainerClassName = getEditorClassNames({
noBorder: true,
borderOnFocus: false,
containerClassName,
});
if (!editor) return null;
return (
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName}>
<div className="flex flex-col">
<EditorContentWrapper tabIndex={tabIndex} editor={editor} />
</div>
</EditorContainer>
);
};
const LiteTextEditorWithRef = React.forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
<LiteTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
));
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";
export { LiteTextEditor, LiteTextEditorWithRef };

View File

@ -1,59 +0,0 @@
import * as React from "react";
import {
EditorContainer,
EditorContentWrapper,
EditorReadOnlyRefApi,
getEditorClassNames,
IMentionHighlight,
useReadOnlyEditor,
} from "@plane/editor-core";
export interface ILiteTextReadOnlyEditor {
initialValue: string;
borderOnFocus?: boolean;
containerClassName?: string;
editorClassName?: string;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
};
tabIndex?: number;
}
const LiteTextReadOnlyEditor = ({
containerClassName,
editorClassName = "",
initialValue,
forwardedRef,
mentionHandler,
tabIndex,
}: ILiteTextReadOnlyEditor) => {
const editor = useReadOnlyEditor({
initialValue,
editorClassName,
forwardedRef,
mentionHandler,
});
const editorContainerClassName = getEditorClassNames({
containerClassName,
});
if (!editor) return null;
return (
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName}>
<div className="flex flex-col">
<EditorContentWrapper tabIndex={tabIndex} editor={editor} />
</div>
</EditorContainer>
);
};
const LiteTextReadOnlyEditorWithRef = React.forwardRef<EditorReadOnlyRefApi, ILiteTextReadOnlyEditor>((props, ref) => (
<LiteTextReadOnlyEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
));
LiteTextReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef";
export { LiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef };

View File

@ -1,6 +0,0 @@
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
module.exports = {
// prefix ui lib classes to avoid conflicting with the app
...sharedConfig,
};

View File

@ -1,15 +0,0 @@
{
"extends": "tsconfig/react-library.json",
"include": [
"src/**/*",
"index.d.ts"
],
"exclude": [
"dist",
"build",
"node_modules"
],
"compilerOptions": {
"baseUrl": "."
}
}

View File

@ -1,11 +0,0 @@
import { defineConfig, Options } from "tsup";
export default defineConfig((options: Options) => ({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
clean: false,
external: ["react"],
injectStyle: true,
...options,
}));

View File

@ -1,5 +1,5 @@
{
"name": "@plane/editor-core",
"name": "@plane/editor",
"version": "0.21.0",
"description": "Core Editor that powers Plane",
"private": true,
@ -28,9 +28,11 @@
"react-dom": "18.2.0"
},
"dependencies": {
"@floating-ui/react": "^0.26.4",
"@plane/ui": "*",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-blockquote": "^2.1.13",
"@tiptap/extension-collaboration": "^2.3.2",
"@tiptap/extension-image": "^2.1.13",
"@tiptap/extension-list-item": "^2.1.13",
"@tiptap/extension-mention": "^2.1.13",
@ -54,7 +56,11 @@
"react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9"
"tiptap-markdown": "^0.8.9",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.5",
"y-protocols": "^1.0.6",
"yjs": "^13.6.15"
},
"devDependencies": {
"@types/node": "18.15.3",

View File

@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -1,6 +0,0 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -1,5 +0,0 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1,98 +0,0 @@
# @plane/rich-text-editor
## Description
The `@plane/rich-text-editor` package extends from the `editor-core` package, inheriting its base functionality while adding its own unique features of Slash Commands and many more.
## Key Features
- **Exported Components**: There are two components exported from the Rich text editor (with and without Ref), you can choose to use the `withRef` instance whenever you want to control the Editors state via a side effect of some external action from within the application code.
`RichTextEditor` & `RichTextEditorWithRef`
- **Read Only Editor Instances**: We have added a really light weight _Read Only_ Editor instance for the Rich editor types (with and without Ref)
`RichReadOnlyEditor` &`RichReadOnlyEditorWithRef`
## RichTextEditor
| Prop | Type | Description |
| ------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uploadFile` | `(file: File) => Promise<string>` | A function that handles file upload. It takes a file as input and handles the process of uploading that file. |
| `deleteFile` | `(assetUrlWithWorkspaceId: string) => Promise<any>` | A function that handles deleting an image. It takes the asset url from your bucket and handles the process of deleting that image. |
| `value` | `html string` | The initial content of the editor. |
| `debouncedUpdatesEnabled` | `boolean` | If set to true, the `onChange` event handler is debounced, meaning it will only be invoked after the specified delay (default 1500ms) once the user has stopped typing. |
| `onChange` | `(json: any, html: string) => void` | This function is invoked whenever the content of the editor changes. It is passed the new content in both JSON and HTML formats. |
| `setIsSubmitting` | `(isSubmitting: "submitting" \| "submitted" \| "saved") => void` | This function is called to update the submission status. |
| `setShouldShowAlert` | `(showAlert: boolean) => void` | This function is used to show or hide an alert incase of content not being "saved". |
| `noBorder` | `boolean` | If set to true, the editor will not have a border. |
| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. |
| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. |
| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. |
### Usage
1. Here is an example of how to use the `RichTextEditor` component
```tsx
<RichTextEditor
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
value={value}
debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting}
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
noBorder={!isAllowed}
onChange={(description: Object, description_html: string) => {
setShowAlert(true);
setIsSubmitting("submitting");
onChange(description_html);
// custom stuff you want to do
}}
/>
```
2. Example of how to use the `RichTextEditorWithRef` component
```tsx
const editorRef = useRef<any>(null);
// can use it to set the editor's value
editorRef.current?.setEditorValue(`${watch("description_html")}`);
// can use it to clear the editor
editorRef?.current?.clearEditor();
return (
<RichTextEditorWithRef
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
deleteFile={fileService.deleteImage}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={value}
customClassName="min-h-[150px]"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
// custom stuff you want to do
}}
/>
);
```
## RichReadOnlyEditor
| Prop | Type | Description |
| ------------------------------- | ------------- | --------------------------------------------------------------------- |
| `value` | `html string` | The initial content of the editor. |
| `noBorder` | `boolean` | If set to true, the editor will not have a border. |
| `borderOnFocus` | `boolean` | If set to true, the editor will show a border when it is focused. |
| `customClassName` | `string` | This is a custom CSS class that can be applied to the editor. |
| `editorContentCustomClassNames` | `string` | This is a custom CSS class that can be applied to the editor content. |
### Usage
Here is an example of how to use the `RichReadOnlyEditor` component
```tsx
<RichReadOnlyEditor value={issueDetails.description_html} customClassName="p-3 min-h-[50px] shadow-sm" />
```

View File

@ -1,56 +0,0 @@
{
"name": "@plane/rich-text-editor",
"version": "0.21.0",
"description": "Rich Text Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": [
"dist/**/*"
],
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"module": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsup --minify",
"dev": "tsup --watch",
"check-types": "tsc --noEmit",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"peerDependencies": {
"next": "12.3.2",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0"
},
"dependencies": {
"@plane/editor-core": "*",
"@plane/editor-extensions": "*",
"@tiptap/core": "^2.1.13",
"lucide-react": "^0.378.0"
},
"devDependencies": {
"@types/node": "18.15.3",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"eslint-config-custom": "*",
"postcss": "^8.4.38",
"react": "^18.2.0",
"tailwind-config-custom": "*",
"tsconfig": "*",
"tsup": "^7.2.0",
"typescript": "4.9.5"
},
"keywords": [
"editor",
"rich-text",
"markdown",
"nextjs",
"react"
]
}

View File

@ -1,9 +0,0 @@
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,8 +0,0 @@
export { RichTextEditor, RichTextEditorWithRef } from "src/ui";
export { RichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef } from "src/ui/read-only";
export type { IRichTextEditor } from "src/ui";
export type { IRichTextReadOnlyEditor } from "src/ui/read-only";
export type { IMentionSuggestion, IMentionHighlight } from "@plane/editor-core";
export type { EditorRefApi, EditorReadOnlyRefApi } from "@plane/editor-core";

View File

@ -1,25 +0,0 @@
import { Extension } from "@tiptap/core";
export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
Extension.create({
name: "enterKey",
addKeyboardShortcuts(this) {
return {
Enter: () => {
if (onEnterKeyPress) {
onEnterKeyPress();
}
return true;
},
"Shift-Enter": ({ editor }) =>
editor.commands.first(({ commands }) => [
() => commands.newlineInCode(),
() => commands.splitListItem("listItem"),
() => commands.createParagraphNear(),
() => commands.liftEmptyBlock(),
() => commands.splitBlock(),
]),
};
},
});

View File

@ -1,22 +0,0 @@
import { UploadImage } from "@plane/editor-core";
import { DragAndDrop, SlashCommand } from "@plane/editor-extensions";
import { EnterKeyExtension } from "./enter-key-extension";
type TArguments = {
uploadFile: UploadImage;
dragDropEnabled?: boolean;
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
onEnterKeyPress?: () => void;
};
export const RichTextEditorExtensions = ({
uploadFile,
dragDropEnabled,
setHideDragHandle,
onEnterKeyPress,
}: TArguments) => [
SlashCommand(uploadFile),
dragDropEnabled === true && DragAndDrop(setHideDragHandle),
// TODO; add the extension conditionally for forms that don't require it
// EnterKeyExtension(onEnterKeyPress),
];

View File

@ -1,113 +0,0 @@
"use client";
import * as React from "react";
// editor-core
import {
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
IMentionHighlight,
IMentionSuggestion,
useEditor,
EditorRefApi,
TFileHandler,
} from "@plane/editor-core";
// extensions
import { RichTextEditorExtensions } from "src/ui/extensions";
// components
import { EditorBubbleMenu } from "src/ui/menus/bubble-menu";
export type IRichTextEditor = {
initialValue: string;
value?: string | null;
dragDropEnabled?: boolean;
fileHandler: TFileHandler;
id?: string;
containerClassName?: string;
editorClassName?: string;
onChange?: (json: object, html: string) => void;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
debouncedUpdatesEnabled?: boolean;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
suggestions: () => Promise<IMentionSuggestion[]>;
};
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
onEnterKeyPress?: (e?: any) => void;
};
const RichTextEditor = (props: IRichTextEditor) => {
const {
onChange,
dragDropEnabled,
initialValue,
value,
fileHandler,
containerClassName,
editorClassName = "",
forwardedRef,
// rerenderOnPropsChange,
id = "",
placeholder,
tabIndex,
mentionHandler,
onEnterKeyPress,
} = props;
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = React.useState<() => void>(() => {});
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
// loads such that we can invoke it from react when the cursor leaves the container
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
};
const editor = useEditor({
id,
editorClassName,
fileHandler,
onChange,
initialValue,
value,
forwardedRef,
// rerenderOnPropsChange,
extensions: RichTextEditorExtensions({
uploadFile: fileHandler.upload,
dragDropEnabled,
setHideDragHandle: setHideDragHandleFunction,
onEnterKeyPress,
}),
tabIndex,
mentionHandler,
placeholder,
});
const editorContainerClassName = getEditorClassNames({
noBorder: true,
borderOnFocus: false,
containerClassName,
});
if (!editor) return null;
return (
<EditorContainer
hideDragHandle={hideDragHandleOnMouseLeave}
editor={editor}
editorContainerClassName={editorContainerClassName}
>
{editor && <EditorBubbleMenu editor={editor} />}
<div className="flex flex-col">
<EditorContentWrapper tabIndex={tabIndex} editor={editor} />
</div>
</EditorContainer>
);
};
const RichTextEditorWithRef = React.forwardRef<EditorRefApi, IRichTextEditor>((props, ref) => (
<RichTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
));
RichTextEditorWithRef.displayName = "RichTextEditorWithRef";
export { RichTextEditor, RichTextEditorWithRef };

View File

@ -1,54 +0,0 @@
"use client";
import {
EditorReadOnlyRefApi,
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
IMentionHighlight,
useReadOnlyEditor,
} from "@plane/editor-core";
import * as React from "react";
export interface IRichTextReadOnlyEditor {
initialValue: string;
containerClassName?: string;
editorClassName?: string;
tabIndex?: number;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
};
}
const RichTextReadOnlyEditor = (props: IRichTextReadOnlyEditor) => {
const { containerClassName, editorClassName = "", initialValue, forwardedRef, mentionHandler } = props;
const editor = useReadOnlyEditor({
initialValue,
editorClassName,
forwardedRef,
mentionHandler,
});
const editorContainerClassName = getEditorClassNames({
containerClassName,
});
if (!editor) return null;
return (
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName}>
<div className="flex flex-col">
<EditorContentWrapper editor={editor} />
</div>
</EditorContainer>
);
};
const RichTextReadOnlyEditorWithRef = React.forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => (
<RichTextReadOnlyEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
));
RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";
export { RichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef };

View File

@ -1,6 +0,0 @@
const sharedConfig = require("tailwind-config-custom/tailwind.config.js");
module.exports = {
// prefix ui lib classes to avoid conflicting with the app
...sharedConfig,
};

View File

@ -1,15 +0,0 @@
{
"extends": "tsconfig/react-library.json",
"include": [
"src/**/*",
"index.d.ts"
],
"exclude": [
"dist",
"build",
"node_modules"
],
"compilerOptions": {
"baseUrl": "."
}
}

View File

@ -1,11 +0,0 @@
import { defineConfig, Options } from "tsup";
export default defineConfig((options: Options) => ({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
clean: false,
external: ["react"],
injectStyle: true,
...options,
}));

View File

@ -0,0 +1 @@
export * from "./collaboration-provider";

View File

@ -0,0 +1 @@
export * from "./issue-embed";

View File

@ -0,0 +1,17 @@
export type TEmbedConfig = {
issue?: TIssueEmbedConfig;
};
export type TReadOnlyEmbedConfig = TEmbedConfig;
export type TIssueEmbedConfig = {
widgetCallback: ({
issueId,
projectId,
workspaceSlug,
}: {
issueId: string;
projectId: string | undefined;
workspaceSlug: string | undefined;
}) => React.ReactNode;
};

View File

@ -1,47 +1,48 @@
import React, { useState } from "react";
// editor-core
import {
getEditorClassNames,
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
TFileHandler,
} from "@plane/editor-core";
// components
import { PageRenderer } from "src/ui/components/page-renderer";
import { PageRenderer } from "@/components/editors";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useDocumentEditor } from "src/hooks/use-document-editor";
import { useDocumentEditor } from "@/hooks/use-document-editor";
import { TFileHandler } from "@/hooks/use-editor";
// plane editor types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion } from "@/types";
interface IDocumentEditor {
id: string;
value: Uint8Array;
fileHandler: TFileHandler;
handleEditorReady?: (value: boolean) => void;
containerClassName?: string;
editorClassName?: string;
onChange: (updates: Uint8Array) => void;
embedHandler: TEmbedConfig;
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
handleEditorReady?: (value: boolean) => void;
id: string;
mentionHandler: {
highlights: () => Promise<IMentionHighlight[]>;
suggestions: () => Promise<IMentionSuggestion[]>;
};
tabIndex?: number;
onChange: (updates: Uint8Array) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
value: Uint8Array;
}
const DocumentEditor = (props: IDocumentEditor) => {
const {
onChange,
id,
value,
fileHandler,
containerClassName,
editorClassName = "",
mentionHandler,
handleEditorReady,
embedHandler,
fileHandler,
forwardedRef,
tabIndex,
handleEditorReady,
id,
mentionHandler,
onChange,
placeholder,
tabIndex,
value,
} = props;
// states
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
@ -55,6 +56,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
const editor = useDocumentEditor({
id,
editorClassName,
embedHandler,
fileHandler,
value,
onChange,
@ -90,4 +92,4 @@ const DocumentEditorWithRef = React.forwardRef<EditorRefApi, IDocumentEditor>((p
DocumentEditorWithRef.displayName = "DocumentEditorWithRef";
export { DocumentEditor, DocumentEditorWithRef };
export { DocumentEditorWithRef };

View File

@ -0,0 +1,3 @@
export * from "./editor";
export * from "./page-renderer";
export * from "./read-only-editor";

View File

@ -1,9 +1,4 @@
import { useCallback, useRef, useState } from "react";
import { EditorContainer, EditorContentWrapper } from "@plane/editor-core";
import { Node } from "@tiptap/pm/model";
import { EditorView } from "@tiptap/pm/view";
import { Editor, ReactRenderer } from "@tiptap/react";
import { LinkView, LinkViewProps } from "./links/link-view";
import {
autoUpdate,
computePosition,
@ -14,7 +9,13 @@ import {
useFloating,
useInteractions,
} from "@floating-ui/react";
import BlockMenu from "../menu//block-menu";
import { Node } from "@tiptap/pm/model";
import { EditorView } from "@tiptap/pm/view";
import { Editor, ReactRenderer } from "@tiptap/react";
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links";
import { BlockMenu } from "@/components/menus";
type IPageRenderer = {
editor: Editor;

View File

@ -1,13 +1,22 @@
import { forwardRef, MutableRefObject } from "react";
import { EditorReadOnlyRefApi, getEditorClassNames, IMentionHighlight, useReadOnlyEditor } from "@plane/editor-core";
// components
import { PageRenderer } from "src/ui/components/page-renderer";
import { IssueWidgetPlaceholder } from "../extensions/widgets/issue-embed-widget";
import { PageRenderer } from "@/components/editors";
// extensions
import { IssueWidget } from "@/extensions";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// plane web types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types";
interface IDocumentReadOnlyEditor {
initialValue: string;
containerClassName: string;
editorClassName?: string;
embedHandler: TEmbedConfig;
tabIndex?: number;
handleEditorReady?: (value: boolean) => void;
mentionHandler: {
@ -20,6 +29,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
const {
containerClassName,
editorClassName = "",
embedHandler,
initialValue,
forwardedRef,
tabIndex,
@ -32,7 +42,12 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
mentionHandler,
forwardedRef,
handleEditorReady,
extensions: [IssueWidgetPlaceholder()],
extensions: [
embedHandler?.issue &&
IssueWidget({
widgetCallback: embedHandler?.issue.widgetCallback,
}),
],
});
if (!editor) {
@ -52,4 +67,4 @@ const DocumentReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IDocument
DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef";
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef };
export { DocumentReadOnlyEditorWithRef };

View File

@ -1,6 +1,7 @@
import { Editor } from "@tiptap/react";
import { FC, ReactNode } from "react";
import { cn } from "src/lib/utils";
import { Editor } from "@tiptap/react";
// helpers
import { cn } from "@/helpers/common";
interface EditorContainerProps {
editor: Editor | null;

View File

@ -1,6 +1,6 @@
import { Editor, EditorContent } from "@tiptap/react";
import { FC, ReactNode } from "react";
import { ImageResizer } from "src/ui/extensions/image/image-resize";
import { ImageResizer } from "src/core/extensions/image/image-resize";
interface EditorContentProps {
editor: Editor | null;

View File

@ -0,0 +1,69 @@
import { Editor, Extension } from "@tiptap/core";
// components
import { EditorContainer } from "@/components/editors";
// hooks
import { getEditorClassNames } from "@/helpers/common";
import { useEditor } from "@/hooks/use-editor";
// types
import { IEditorProps } from "@/types";
import { EditorContentWrapper } from "./editor-content";
type Props = IEditorProps & {
children?: (editor: Editor) => React.ReactNode;
extensions: Extension<any, any>[];
hideDragHandleOnMouseLeave: () => void;
};
export const EditorWrapper: React.FC<Props> = (props) => {
const {
children,
containerClassName,
editorClassName = "",
extensions,
hideDragHandleOnMouseLeave,
id = "",
initialValue,
fileHandler,
forwardedRef,
mentionHandler,
onChange,
placeholder,
tabIndex,
value,
} = props;
const editor = useEditor({
editorClassName,
extensions,
fileHandler,
forwardedRef,
id,
initialValue,
mentionHandler,
onChange,
placeholder,
tabIndex,
value,
});
const editorContainerClassName = getEditorClassNames({
noBorder: true,
borderOnFocus: false,
containerClassName,
});
if (!editor) return null;
return (
<EditorContainer
hideDragHandle={hideDragHandleOnMouseLeave}
editor={editor}
editorContainerClassName={editorContainerClassName}
>
{children?.(editor)}
<div className="flex flex-col">
<EditorContentWrapper tabIndex={tabIndex} editor={editor} />
</div>
</EditorContainer>
);
};

View File

@ -0,0 +1,7 @@
export * from "./document";
export * from "./lite-text";
export * from "./rich-text";
export * from "./editor-container";
export * from "./editor-content";
export * from "./editor-wrapper";
export * from "./read-only-editor-wrapper";

View File

@ -0,0 +1,23 @@
import { forwardRef } from "react";
// components
import { EditorWrapper } from "@/components/editors/editor-wrapper";
// extensions
import { EnterKeyExtension } from "@/extensions";
// types
import { EditorRefApi, ILiteTextEditor } from "@/types";
const LiteTextEditor = (props: ILiteTextEditor) => {
const { onEnterKeyPress } = props;
const extensions = [EnterKeyExtension(onEnterKeyPress)];
return <EditorWrapper {...props} extensions={extensions} hideDragHandleOnMouseLeave={() => {}} />;
};
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
<LiteTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
));
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";
export { LiteTextEditorWithRef };

View File

@ -0,0 +1,2 @@
export * from "./editor";
export * from "./read-only-editor";

View File

@ -0,0 +1,13 @@
import { forwardRef } from "react";
// components
import { ReadOnlyEditorWrapper } from "@/components/editors";
// types
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor } from "@/types";
const LiteTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, ILiteTextReadOnlyEditor>((props, ref) => (
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
));
LiteTextReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef";
export { LiteTextReadOnlyEditorWithRef };

View File

@ -0,0 +1,33 @@
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
import { IReadOnlyEditorProps } from "@/types";
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
const { containerClassName, editorClassName = "", initialValue, forwardedRef, mentionHandler } = props;
const editor = useReadOnlyEditor({
initialValue,
editorClassName,
forwardedRef,
mentionHandler,
});
const editorContainerClassName = getEditorClassNames({
containerClassName,
});
if (!editor) return null;
return (
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName}>
<div className="flex flex-col">
<EditorContentWrapper editor={editor} />
</div>
</EditorContainer>
);
};

View File

@ -0,0 +1,46 @@
import { forwardRef, useCallback, useState } from "react";
// components
import { EditorWrapper } from "@/components/editors";
import { EditorBubbleMenu } from "@/components/menus";
// extensions
import { DragAndDrop, SlashCommand } from "@/extensions";
// types
import { EditorRefApi, IRichTextEditor } from "@/types";
const RichTextEditor = (props: IRichTextEditor) => {
const { dragDropEnabled, fileHandler } = props;
// states
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
// loads such that we can invoke it from react when the cursor leaves the container
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
};
const getExtensions = useCallback(() => {
const extensions = [
SlashCommand(fileHandler.upload),
// TODO; add the extension conditionally for forms that don't require it
// EnterKeyExtension(onEnterKeyPress),
];
if (dragDropEnabled) extensions.push(DragAndDrop(setHideDragHandleFunction));
return extensions;
}, [dragDropEnabled, fileHandler.upload]);
return (
<EditorWrapper {...props} extensions={getExtensions()} hideDragHandleOnMouseLeave={hideDragHandleOnMouseLeave}>
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
</EditorWrapper>
);
};
const RichTextEditorWithRef = forwardRef<EditorRefApi, IRichTextEditor>((props, ref) => (
<RichTextEditor {...props} forwardedRef={ref as React.MutableRefObject<EditorRefApi | null>} />
));
RichTextEditorWithRef.displayName = "RichTextEditorWithRef";
export { RichTextEditorWithRef };

View File

@ -0,0 +1,2 @@
export * from "./editor";
export * from "./read-only-editor";

View File

@ -0,0 +1,12 @@
import { forwardRef } from "react";
// types
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types";
import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper";
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => (
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
));
RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";
export { RichTextReadOnlyEditorWithRef };

Some files were not shown because too many files have changed in this diff Show More