mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[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:
parent
367ccba17e
commit
dcdd1ef065
@ -7,7 +7,7 @@
|
||||
"web",
|
||||
"space",
|
||||
"admin",
|
||||
"packages/editor/*",
|
||||
"packages/editor",
|
||||
"packages/eslint-config-custom",
|
||||
"packages/tailwind-config-custom",
|
||||
"packages/tsconfig",
|
||||
|
||||
@ -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 Editor’s 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
|
||||
@ -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";
|
||||
@ -1 +0,0 @@
|
||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
@ -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;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
import { Smile } from "lucide-react";
|
||||
|
||||
export type LucideIconType = typeof Smile;
|
||||
@ -1 +0,0 @@
|
||||
export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
@ -1 +0,0 @@
|
||||
export type UploadImage = (file: File) => Promise<string>;
|
||||
@ -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)];
|
||||
},
|
||||
});
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
# Document Editor
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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: {},
|
||||
},
|
||||
};
|
||||
@ -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";
|
||||
@ -1,6 +0,0 @@
|
||||
export interface IMarking {
|
||||
type: "heading";
|
||||
level: number;
|
||||
text: string;
|
||||
sequence: number;
|
||||
}
|
||||
@ -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>;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from "./page-renderer";
|
||||
@ -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,
|
||||
}),
|
||||
];
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
@ -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;
|
||||
};
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
import { IssueWidget } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-node";
|
||||
|
||||
export const IssueWidgetPlaceholder = () => IssueWidget.configure({});
|
||||
@ -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>
|
||||
);
|
||||
@ -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)];
|
||||
},
|
||||
});
|
||||
@ -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>
|
||||
);
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}));
|
||||
@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@ -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 Editor’s 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"
|
||||
/>
|
||||
```
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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: {},
|
||||
},
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./drag-drop";
|
||||
export * from "./slash-commands";
|
||||
@ -1,3 +0,0 @@
|
||||
import "src/styles/drag-drop.css";
|
||||
|
||||
export { DragAndDrop, SlashCommand } from "src/extensions";
|
||||
@ -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,
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}));
|
||||
@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@ -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 Editor’s 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"
|
||||
/>
|
||||
```
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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: {},
|
||||
},
|
||||
};
|
||||
@ -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";
|
||||
@ -1,3 +0,0 @@
|
||||
import { EnterKeyExtension } from "src/ui/extensions/enter-key-extension";
|
||||
|
||||
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [EnterKeyExtension(onEnterKeyPress)];
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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,
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}));
|
||||
@ -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",
|
||||
@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@ -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 Editor’s 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" />
|
||||
```
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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: {},
|
||||
},
|
||||
};
|
||||
@ -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";
|
||||
@ -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(),
|
||||
]),
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -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),
|
||||
];
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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,
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}));
|
||||
1
packages/editor/src/ce/providers/index.ts
Normal file
1
packages/editor/src/ce/providers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./collaboration-provider";
|
||||
1
packages/editor/src/ce/types/index.ts
Normal file
1
packages/editor/src/ce/types/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./issue-embed";
|
||||
17
packages/editor/src/ce/types/issue-embed.ts
Normal file
17
packages/editor/src/ce/types/issue-embed.ts
Normal 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;
|
||||
};
|
||||
@ -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 };
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./editor";
|
||||
export * from "./page-renderer";
|
||||
export * from "./read-only-editor";
|
||||
@ -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;
|
||||
@ -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 };
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
7
packages/editor/src/core/components/editors/index.ts
Normal file
7
packages/editor/src/core/components/editors/index.ts
Normal 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";
|
||||
@ -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 };
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./editor";
|
||||
export * from "./read-only-editor";
|
||||
@ -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 };
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 };
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./editor";
|
||||
export * from "./read-only-editor";
|
||||
@ -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
Loading…
Reference in New Issue
Block a user