mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
1 Commits
feat-chang
...
feat/page-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5888f68b7b |
@@ -6,7 +6,7 @@ import { COLORS_LIST } from "@/constants/common";
|
||||
import { CalloutBlockColorSelector } from "./color-selector";
|
||||
import { CalloutBlockLogoSelector } from "./logo-selector";
|
||||
// types
|
||||
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
|
||||
import { ECalloutAttributeNames, TCalloutBlockAttributes } from "./types";
|
||||
// utils
|
||||
import { updateStoredBackgroundColor } from "./utils";
|
||||
|
||||
@@ -45,7 +45,7 @@ export const CustomCalloutBlock: React.FC<Props> = (props) => {
|
||||
toggleDropdown={() => setIsColorPickerOpen((prev) => !prev)}
|
||||
onSelect={(val) => {
|
||||
updateAttributes({
|
||||
[EAttributeNames.BACKGROUND]: val,
|
||||
[ECalloutAttributeNames.BACKGROUND]: val,
|
||||
});
|
||||
updateStoredBackgroundColor(val);
|
||||
}}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
// types
|
||||
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
|
||||
import { ECalloutAttributeNames, TCalloutBlockAttributes } from "./types";
|
||||
// utils
|
||||
import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils";
|
||||
|
||||
@@ -23,7 +23,7 @@ export const CustomCalloutExtensionConfig = Node.create({
|
||||
addAttributes() {
|
||||
const attributes = {
|
||||
// Reduce instead of map to accumulate the attributes directly into an object
|
||||
...Object.values(EAttributeNames).reduce((acc, value) => {
|
||||
...Object.values(ECalloutAttributeNames).reduce((acc, value) => {
|
||||
acc[value] = {
|
||||
default: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[value],
|
||||
};
|
||||
@@ -60,7 +60,7 @@ export const CustomCalloutExtensionConfig = Node.create({
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[${EAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[EAttributeNames.BLOCK_TYPE]}"]`,
|
||||
tag: `div[${ECalloutAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[ECalloutAttributeNames.BLOCK_TYPE]}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { findParentNodeClosestToPos, Predicate, ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomCalloutBlock } from "@/extensions";
|
||||
// helpers
|
||||
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
|
||||
// block
|
||||
import { CustomCalloutBlock } from "./block";
|
||||
// config
|
||||
import { CustomCalloutExtensionConfig } from "./extension-config";
|
||||
// types
|
||||
import { ECalloutAttributeNames } from "./types";
|
||||
// utils
|
||||
import { getStoredBackgroundColor, getStoredLogo } from "./utils";
|
||||
|
||||
@@ -30,7 +32,7 @@ export const CustomCalloutExtension = CustomCalloutExtensionConfig.extend({
|
||||
],
|
||||
attrs: {
|
||||
...storedLogoValues,
|
||||
"data-background": storedBackgroundValue,
|
||||
[ECalloutAttributeNames.BACKGROUND]: storedBackgroundValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./block";
|
||||
export * from "./extension";
|
||||
export * from "./read-only-extension";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomCalloutBlock } from "@/extensions";
|
||||
// block
|
||||
import { CustomCalloutBlock } from "./block";
|
||||
// config
|
||||
import { CustomCalloutExtensionConfig } from "./extension-config";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum EAttributeNames {
|
||||
export enum ECalloutAttributeNames {
|
||||
ICON_COLOR = "data-icon-color",
|
||||
ICON_NAME = "data-icon-name",
|
||||
EMOJI_UNICODE = "data-emoji-unicode",
|
||||
@@ -9,18 +9,18 @@ export enum EAttributeNames {
|
||||
}
|
||||
|
||||
export type TCalloutBlockIconAttributes = {
|
||||
[EAttributeNames.ICON_COLOR]: string | undefined;
|
||||
[EAttributeNames.ICON_NAME]: string | undefined;
|
||||
[ECalloutAttributeNames.ICON_COLOR]: string | undefined;
|
||||
[ECalloutAttributeNames.ICON_NAME]: string | undefined;
|
||||
};
|
||||
|
||||
export type TCalloutBlockEmojiAttributes = {
|
||||
[EAttributeNames.EMOJI_UNICODE]: string | undefined;
|
||||
[EAttributeNames.EMOJI_URL]: string | undefined;
|
||||
[ECalloutAttributeNames.EMOJI_UNICODE]: string | undefined;
|
||||
[ECalloutAttributeNames.EMOJI_URL]: string | undefined;
|
||||
};
|
||||
|
||||
export type TCalloutBlockAttributes = {
|
||||
[EAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
|
||||
[EAttributeNames.BACKGROUND]: string;
|
||||
[EAttributeNames.BLOCK_TYPE]: "callout-component";
|
||||
[ECalloutAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
|
||||
[ECalloutAttributeNames.BACKGROUND]: string;
|
||||
[ECalloutAttributeNames.BLOCK_TYPE]: "callout-component";
|
||||
} & TCalloutBlockIconAttributes &
|
||||
TCalloutBlockEmojiAttributes;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { sanitizeHTML } from "@plane/helpers";
|
||||
import { TEmojiLogoProps } from "@plane/ui";
|
||||
// types
|
||||
import {
|
||||
EAttributeNames,
|
||||
ECalloutAttributeNames,
|
||||
TCalloutBlockAttributes,
|
||||
TCalloutBlockEmojiAttributes,
|
||||
TCalloutBlockIconAttributes,
|
||||
@@ -20,7 +20,7 @@ export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = {
|
||||
"data-block-type": "callout-component",
|
||||
};
|
||||
|
||||
type TStoredLogoValue = Pick<TCalloutBlockAttributes, EAttributeNames.LOGO_IN_USE> &
|
||||
type TStoredLogoValue = Pick<TCalloutBlockAttributes, ECalloutAttributeNames.LOGO_IN_USE> &
|
||||
(TCalloutBlockEmojiAttributes | TCalloutBlockIconAttributes);
|
||||
|
||||
// function to get the stored logo from local storage
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomTextAlignExtension } from "./text-align";
|
||||
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
|
||||
import { CustomColorExtension } from "./custom-color";
|
||||
import { CustomEmbedExtensionConfig } from "./embed/extension-config";
|
||||
|
||||
export const CoreEditorExtensionsWithoutProps = [
|
||||
StarterKit.configure({
|
||||
@@ -89,6 +90,7 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtensionConfig,
|
||||
CustomColorExtension,
|
||||
CustomEmbedExtensionConfig,
|
||||
];
|
||||
|
||||
export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()];
|
||||
|
||||
81
packages/editor/src/core/extensions/embed/block.tsx
Normal file
81
packages/editor/src/core/extensions/embed/block.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// types
|
||||
import { EEmbedAttributeNames, TEmbedBlockAttributes } from "./types";
|
||||
|
||||
type Props = NodeViewProps & {
|
||||
node: NodeViewProps["node"] & {
|
||||
attrs: TEmbedBlockAttributes;
|
||||
};
|
||||
updateAttributes: (attrs: Partial<TEmbedBlockAttributes>) => void;
|
||||
};
|
||||
|
||||
export const CustomEmbedBlock: React.FC<Props> = (props) => {
|
||||
const { editor, node } = props;
|
||||
// states
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
// derived values
|
||||
const embedSource = node.attrs[EEmbedAttributeNames.SOURCE];
|
||||
const embedWidth = node.attrs[EEmbedAttributeNames.WIDTH];
|
||||
|
||||
const handleResizeStart = () => {
|
||||
setIsResizing(true);
|
||||
};
|
||||
|
||||
const handleResize = useCallback((e: MouseEvent | TouchEvent) => {}, []);
|
||||
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
window.addEventListener("mousemove", handleResize);
|
||||
window.addEventListener("mouseup", handleResizeEnd);
|
||||
window.addEventListener("mouseleave", handleResizeEnd);
|
||||
window.addEventListener("touchmove", handleResize);
|
||||
window.addEventListener("touchend", handleResizeEnd);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleResize);
|
||||
window.removeEventListener("mouseup", handleResizeEnd);
|
||||
window.removeEventListener("mouseleave", handleResizeEnd);
|
||||
window.removeEventListener("touchmove", handleResize);
|
||||
window.removeEventListener("touchend", handleResizeEnd);
|
||||
};
|
||||
}
|
||||
}, [isResizing, handleResize, handleResizeEnd]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className="editor-embed-component group/embed-component relative my-2">
|
||||
<iframe className="rounded-md" src={embedSource} width={embedWidth} />
|
||||
{editor.isEditable && (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 border-2 border-custom-primary-100 pointer-events-none rounded-md transition-opacity duration-100 ease-in-out",
|
||||
{
|
||||
"opacity-100": isResizing,
|
||||
"opacity-0 group-hover/embed-component:opacity-100": !isResizing,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 right-0 translate-y-1/2 translate-x-1/2 size-4 rounded-full bg-custom-primary-100 border-2 border-white cursor-nwse-resize transition-opacity duration-100 ease-in-out",
|
||||
{
|
||||
"opacity-100 pointer-events-auto": isResizing,
|
||||
"opacity-0 pointer-events-none group-hover/embed-component:opacity-100 group-hover/embed-component:pointer-events-auto":
|
||||
!isResizing,
|
||||
}
|
||||
)}
|
||||
onMouseDown={handleResizeStart}
|
||||
onTouchStart={handleResizeStart}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
// types
|
||||
import { EEmbedAttributeNames } from "./types";
|
||||
// utils
|
||||
import { DEFAULT_EMBED_BLOCK_ATTRIBUTES } from "./utils";
|
||||
|
||||
// Extend Tiptap's Commands interface
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
embedComponent: {
|
||||
insertEmbed: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomEmbedExtensionConfig = Node.create({
|
||||
name: "embedComponent",
|
||||
group: "block",
|
||||
atom: true,
|
||||
inline: false,
|
||||
|
||||
addAttributes() {
|
||||
const attributes = {
|
||||
// Reduce instead of map to accumulate the attributes directly into an object
|
||||
...Object.values(EEmbedAttributeNames).reduce((acc, value) => {
|
||||
acc[value] = {
|
||||
default: DEFAULT_EMBED_BLOCK_ATTRIBUTES[value],
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
return attributes;
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: NodeType) {},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[${EEmbedAttributeNames.BLOCK_TYPE}="${DEFAULT_EMBED_BLOCK_ATTRIBUTES[EEmbedAttributeNames.BLOCK_TYPE]}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// Render HTML for the embed node
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["div", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
});
|
||||
35
packages/editor/src/core/extensions/embed/extension.tsx
Normal file
35
packages/editor/src/core/extensions/embed/extension.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// block
|
||||
import { CustomEmbedBlock } from "./block";
|
||||
// config
|
||||
import { CustomEmbedExtensionConfig } from "./extension-config";
|
||||
// types
|
||||
import { EEmbedAttributeNames } from "./types";
|
||||
|
||||
export const CustomEmbedExtension = CustomEmbedExtensionConfig.extend({
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
insertEmbed:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: {
|
||||
[EEmbedAttributeNames.SOURCE]: "https://www.youtube.com/embed/z5rd-ZE2f3I?si=H9bkPwKyJtCXdRGW",
|
||||
[EEmbedAttributeNames.WIDTH]: "100%",
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomEmbedBlock);
|
||||
},
|
||||
});
|
||||
1
packages/editor/src/core/extensions/embed/index.ts
Normal file
1
packages/editor/src/core/extensions/embed/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./extension";
|
||||
11
packages/editor/src/core/extensions/embed/types.ts
Normal file
11
packages/editor/src/core/extensions/embed/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum EEmbedAttributeNames {
|
||||
SOURCE = "src",
|
||||
WIDTH = "width",
|
||||
BLOCK_TYPE = "data-block-type",
|
||||
}
|
||||
|
||||
export type TEmbedBlockAttributes = {
|
||||
[EEmbedAttributeNames.SOURCE]: string | undefined;
|
||||
[EEmbedAttributeNames.WIDTH]: string | number | undefined;
|
||||
[EEmbedAttributeNames.BLOCK_TYPE]: "embed-component";
|
||||
};
|
||||
8
packages/editor/src/core/extensions/embed/utils.ts
Normal file
8
packages/editor/src/core/extensions/embed/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// types
|
||||
import { TEmbedBlockAttributes } from "./types";
|
||||
|
||||
export const DEFAULT_EMBED_BLOCK_ATTRIBUTES: TEmbedBlockAttributes = {
|
||||
src: undefined,
|
||||
width: undefined,
|
||||
"data-block-type": "embed-component",
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CustomCodeInlineExtension,
|
||||
CustomCodeMarkPlugin,
|
||||
CustomColorExtension,
|
||||
CustomEmbedExtension,
|
||||
CustomHorizontalRule,
|
||||
CustomImageExtension,
|
||||
CustomKeymap,
|
||||
@@ -162,5 +163,6 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtension,
|
||||
CustomColorExtension,
|
||||
CustomEmbedExtension,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./code-inline";
|
||||
export * from "./custom-image";
|
||||
export * from "./custom-link";
|
||||
export * from "./custom-list-keymap";
|
||||
export * from "./embed";
|
||||
export * from "./image";
|
||||
export * from "./issue-embed";
|
||||
export * from "./mentions";
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ListTodo,
|
||||
MessageSquareText,
|
||||
MinusSquare,
|
||||
ScreenShare,
|
||||
Table,
|
||||
TextQuote,
|
||||
} from "lucide-react";
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
insertImage,
|
||||
insertCallout,
|
||||
setText,
|
||||
insertEmbed,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { CommandProps, ISlashCommandItem } from "@/types";
|
||||
@@ -189,6 +191,15 @@ export const getSlashCommandFilteredSections =
|
||||
searchTerms: ["callout", "comment", "message", "info", "alert"],
|
||||
command: ({ editor, range }: CommandProps) => insertCallout(editor, range),
|
||||
},
|
||||
{
|
||||
commandKey: "embed",
|
||||
key: "embed",
|
||||
title: "Embed",
|
||||
icon: <ScreenShare className="size-3.5" />,
|
||||
description: "Insert embed",
|
||||
searchTerms: ["embed", "hyperlink", "video", "pdf"],
|
||||
command: ({ editor, range }) => insertEmbed(editor, range),
|
||||
},
|
||||
{
|
||||
commandKey: "divider",
|
||||
key: "divider",
|
||||
|
||||
@@ -189,7 +189,13 @@ export const insertHorizontalRule = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
else editor.chain().focus().setHorizontalRule().run();
|
||||
};
|
||||
|
||||
export const insertCallout = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
|
||||
else editor.chain().focus().insertCallout().run();
|
||||
};
|
||||
|
||||
export const insertEmbed = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertEmbed().run();
|
||||
else editor.chain().focus().insertEmbed().run();
|
||||
};
|
||||
|
||||
@@ -39,7 +39,8 @@ export type TEditorCommands =
|
||||
| "text-color"
|
||||
| "background-color"
|
||||
| "text-align"
|
||||
| "callout";
|
||||
| "callout"
|
||||
| "embed";
|
||||
|
||||
export type TCommandExtraProps = {
|
||||
image: {
|
||||
@@ -121,7 +122,7 @@ export interface IEditorProps {
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
tabIndex?: number;
|
||||
value?: string | null;
|
||||
value?: string | null;
|
||||
}
|
||||
export interface ILiteTextEditor extends IEditorProps {
|
||||
extensions?: any[];
|
||||
|
||||
Reference in New Issue
Block a user