[PE-298] Fix: Copy markdown to clipboard (#6675)
* fix: markdown for mentions fixed * fix: copying text in mentions * fix: refactored the component to use the same function * chore: renamed funcion name * add the new copy extension * init working fix * remove useless code * improve readibility * update node import * better smaller logic * remove log * add open close end handler * update readabliity * handle tables * handle triple click in cell * triple tap select current line * handle block and list * lists fixed * handle all possible cases of copy in table * update the min elements * handle multi types in table * handle table seletion cases * handle whole table handler * feat: all case converd * update markdown handling code * update return statement * handle using group block * handle param * handle multple cell in table * handle using recursion * add types * fix code rabbit suggestions * fix root node bug * update recursion with loop * update transform copied to false * refactor clipboard extension: remove options and integrate MarkdownClipboard into core extensions * fix: header and code handler * fix: store hooks fixed * fix: mention id --------- Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
This commit is contained in:
parent
72307ec100
commit
6bafdb6dd8
17 changed files with 225 additions and 40 deletions
89
packages/editor/src/core/extensions/clipboard.ts
Normal file
89
packages/editor/src/core/extensions/clipboard.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { Extension } from "@tiptap/core";
|
||||
import { Fragment, Node } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
|
||||
export const MarkdownClipboard = Extension.create({
|
||||
name: "markdownClipboard",
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("markdownClipboard"),
|
||||
props: {
|
||||
clipboardTextSerializer: (slice) => {
|
||||
const markdownSerializer = this.editor.storage.markdown.serializer;
|
||||
const isTableRow = slice.content.firstChild?.type?.name === "tableRow";
|
||||
const nodeSelect = slice.openStart === 0 && slice.openEnd === 0;
|
||||
|
||||
if (nodeSelect) {
|
||||
return markdownSerializer.serialize(slice.content);
|
||||
}
|
||||
|
||||
const processTableContent = (tableNode: Node | Fragment) => {
|
||||
let result = "";
|
||||
tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => {
|
||||
tableRowNode.content?.forEach?.((cell: Node) => {
|
||||
const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : "";
|
||||
result += cellContent + "\n";
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
if (isTableRow) {
|
||||
const rowsCount = slice.content?.childCount || 0;
|
||||
const cellsCount = slice.content?.firstChild?.content?.childCount || 0;
|
||||
if (rowsCount === 1 || cellsCount === 1) {
|
||||
return processTableContent(slice.content);
|
||||
} else {
|
||||
return markdownSerializer.serialize(slice.content);
|
||||
}
|
||||
}
|
||||
|
||||
const traverseToParentOfLeaf = (
|
||||
node: Node | null,
|
||||
parent: Fragment | Node,
|
||||
depth: number
|
||||
): Node | Fragment => {
|
||||
let currentNode = node;
|
||||
let currentParent = parent;
|
||||
let currentDepth = depth;
|
||||
|
||||
while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) {
|
||||
if (currentNode.content?.childCount > 1) {
|
||||
if (currentNode.content.firstChild?.type?.name === "listItem") {
|
||||
return currentParent;
|
||||
} else {
|
||||
return currentNode.content;
|
||||
}
|
||||
}
|
||||
|
||||
currentParent = currentNode;
|
||||
currentNode = currentNode.content?.firstChild || null;
|
||||
currentDepth--;
|
||||
}
|
||||
|
||||
return currentParent;
|
||||
};
|
||||
|
||||
if (slice.content.childCount > 1) {
|
||||
return markdownSerializer.serialize(slice.content);
|
||||
} else {
|
||||
const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart);
|
||||
|
||||
let currentNode = targetNode;
|
||||
while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) {
|
||||
currentNode = currentNode.firstChild;
|
||||
}
|
||||
if (currentNode instanceof Node && currentNode.isText) {
|
||||
return currentNode.text;
|
||||
}
|
||||
|
||||
return markdownSerializer.serialize(targetNode);
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
@ -29,6 +29,7 @@ import {
|
|||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
MarkdownClipboard,
|
||||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
|
|
@ -130,10 +131,11 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
|||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
transformCopiedText: false,
|
||||
transformPastedText: true,
|
||||
breaks: true,
|
||||
}),
|
||||
MarkdownClipboard,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@ export * from "./quote";
|
|||
export * from "./read-only-extensions";
|
||||
export * from "./side-menu";
|
||||
export * from "./text-align";
|
||||
export * from "./clipboard";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { mergeAttributes } from "@tiptap/core";
|
||||
import Mention, { MentionOptions } from "@tiptap/extension-mention";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
// types
|
||||
import { TMentionHandler } from "@/types";
|
||||
// local types
|
||||
import { EMentionComponentAttributeNames } from "./types";
|
||||
import { EMentionComponentAttributeNames, TMentionComponentAttributes } from "./types";
|
||||
|
||||
export type TMentionExtensionOptions = MentionOptions & {
|
||||
renderComponent: TMentionHandler["renderComponent"];
|
||||
getMentionedEntityDetails: TMentionHandler["getMentionedEntityDetails"];
|
||||
};
|
||||
|
||||
export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOptions>({
|
||||
|
|
@ -40,9 +43,26 @@ export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOpti
|
|||
class: "mention",
|
||||
},
|
||||
|
||||
addStorage(this) {
|
||||
renderText({ node }) {
|
||||
return getMentionDisplayText(this.options, node);
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
const options = this.options;
|
||||
return {
|
||||
mentionsOpen: false,
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: NodeType) {
|
||||
state.write(getMentionDisplayText(options, node));
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function getMentionDisplayText(options: TMentionExtensionOptions, node: NodeType): string {
|
||||
const attrs = node.attrs as TMentionComponentAttributes;
|
||||
const mentionEntityId = attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER];
|
||||
const mentionEntityDetails = options.getMentionedEntityDetails?.(mentionEntityId ?? "");
|
||||
return `@${mentionEntityDetails?.display_name ?? attrs[EMentionComponentAttributeNames.ID] ?? mentionEntityId}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ import { MentionNodeView } from "./mention-node-view";
|
|||
import { renderMentionsDropdown } from "./utils";
|
||||
|
||||
export const CustomMentionExtension = (props: TMentionHandler) => {
|
||||
const { searchCallback, renderComponent } = props;
|
||||
const { searchCallback, renderComponent, getMentionedEntityDetails } = props;
|
||||
return CustomMentionExtensionConfig.extend({
|
||||
addOptions(this) {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
renderComponent,
|
||||
getMentionedEntityDetails,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
CustomColorExtension,
|
||||
MarkdownClipboard,
|
||||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
|
|
@ -114,8 +115,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
|||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
transformCopiedText: false,
|
||||
}),
|
||||
MarkdownClipboard,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,22 @@ export function tableControls() {
|
|||
},
|
||||
},
|
||||
props: {
|
||||
handleTripleClickOn(view, pos, node, nodePos, event, direct) {
|
||||
if (node.type.name === 'tableCell') {
|
||||
event.preventDefault();
|
||||
const $pos = view.state.doc.resolve(pos);
|
||||
const line = $pos.parent;
|
||||
const linePos = $pos.start();
|
||||
const start = linePos;
|
||||
const end = linePos + line.nodeSize - 1;
|
||||
const tr = view.state.tr.setSelection(
|
||||
TextSelection.create(view.state.doc, start, end)
|
||||
);
|
||||
view.dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleDOMEvents: {
|
||||
mousemove: (view, event) => {
|
||||
const pluginState = key.getState(view.state);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// plane types
|
||||
import { TSearchEntities } from "@plane/types";
|
||||
import { IUserLite, TSearchEntities } from "@plane/types";
|
||||
|
||||
export type TMentionSuggestion = {
|
||||
entity_identifier: string;
|
||||
|
|
@ -20,6 +20,7 @@ export type TMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier
|
|||
|
||||
export type TReadOnlyMentionHandler = {
|
||||
renderComponent: (props: TMentionComponentProps) => React.ReactNode;
|
||||
getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;
|
||||
};
|
||||
|
||||
export type TMentionHandler = TReadOnlyMentionHandler & {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue