mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: inline code blocks, code blocks and links have saner behaviour (#3318)
* fix: removed backticks in inline code blocks * added better error handling while cancelling uploads * fix: inline code blocks, code blocks and links have saner behaviour - Inline code blocks are now exitable, don't have backticks, have better padding vertically and better regex matching - Code blocks on the top and bottom of the document are now exitable via Up and Down Arrow keys - Links are now exitable while being autolinkable via a custom re-write of the tiptap-link-extension * fix: more robust link checking
This commit is contained in:
parent
2cd5dbcd02
commit
27762ea500
@ -30,10 +30,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/core": "^2.1.13",
|
"@tiptap/core": "^2.1.13",
|
||||||
"@tiptap/extension-blockquote": "^2.1.13",
|
"@tiptap/extension-blockquote": "^2.1.13",
|
||||||
|
"@tiptap/extension-code": "^2.1.13",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.1.13",
|
"@tiptap/extension-code-block-lowlight": "^2.1.13",
|
||||||
"@tiptap/extension-color": "^2.1.13",
|
"@tiptap/extension-color": "^2.1.13",
|
||||||
"@tiptap/extension-image": "^2.1.13",
|
"@tiptap/extension-image": "^2.1.13",
|
||||||
"@tiptap/extension-link": "^2.1.13",
|
|
||||||
"@tiptap/extension-list-item": "^2.1.13",
|
"@tiptap/extension-list-item": "^2.1.13",
|
||||||
"@tiptap/extension-mention": "^2.1.13",
|
"@tiptap/extension-mention": "^2.1.13",
|
||||||
"@tiptap/extension-task-item": "^2.1.13",
|
"@tiptap/extension-task-item": "^2.1.13",
|
||||||
@ -48,6 +48,7 @@
|
|||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"highlight.js": "^11.8.0",
|
"highlight.js": "^11.8.0",
|
||||||
"jsx-dom-cjs": "^8.0.3",
|
"jsx-dom-cjs": "^8.0.3",
|
||||||
|
"linkifyjs": "^4.1.3",
|
||||||
"lowlight": "^3.0.0",
|
"lowlight": "^3.0.0",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react-moveable": "^0.54.2",
|
"react-moveable": "^0.54.2",
|
||||||
|
@ -12,6 +12,11 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror code::before,
|
||||||
|
.ProseMirror code::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror .is-empty::before {
|
.ProseMirror .is-empty::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
float: left;
|
float: left;
|
||||||
|
31
packages/editor/core/src/ui/extensions/code-inline/index.tsx
Normal file
31
packages/editor/core/src/ui/extensions/code-inline/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { markInputRule, markPasteRule } from "@tiptap/core";
|
||||||
|
import Code from "@tiptap/extension-code";
|
||||||
|
|
||||||
|
export const inputRegex = /(?<!`)`([^`]*)`(?!`)/;
|
||||||
|
export const pasteRegex = /(?<!`)`([^`]+)`(?!`)/g;
|
||||||
|
|
||||||
|
export const CustomCodeInlineExtension = Code.extend({
|
||||||
|
exitable: true,
|
||||||
|
inclusive: false,
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
markInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addPasteRules() {
|
||||||
|
return [
|
||||||
|
markPasteRule({
|
||||||
|
find: pasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}).configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000",
|
||||||
|
spellcheck: "false",
|
||||||
|
},
|
||||||
|
});
|
@ -6,10 +6,61 @@ import ts from "highlight.js/lib/languages/typescript";
|
|||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("ts", ts);
|
lowlight.register("ts", ts);
|
||||||
|
|
||||||
export const CustomCodeBlock = CodeBlockLowlight.extend({
|
import { Selection } from "@tiptap/pm/state";
|
||||||
|
|
||||||
|
export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
|
||||||
addKeyboardShortcuts() {
|
addKeyboardShortcuts() {
|
||||||
return {
|
return {
|
||||||
Tab: ({ editor }) => {
|
Tab: ({ editor }) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { selection } = state;
|
||||||
|
const { $from, empty } = selection;
|
||||||
|
|
||||||
|
if (!empty || $from.parent.type !== this.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ProseMirror's insertText transaction to insert the tab character
|
||||||
|
const tr = state.tr.insertText("\t", $from.pos, $from.pos);
|
||||||
|
editor.view.dispatch(tr);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
ArrowUp: ({ editor }) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { selection } = state;
|
||||||
|
const { $from, empty } = selection;
|
||||||
|
|
||||||
|
if (!empty || $from.parent.type !== this.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAtStart = $from.parentOffset === 0;
|
||||||
|
|
||||||
|
if (!isAtStart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if codeBlock is the first node
|
||||||
|
const isFirstNode = $from.depth === 1 && $from.index($from.depth - 1) === 0;
|
||||||
|
|
||||||
|
if (isFirstNode) {
|
||||||
|
// Insert a new paragraph at the start of the document and move the cursor to it
|
||||||
|
return editor.commands.command(({ tr }) => {
|
||||||
|
const node = editor.schema.nodes.paragraph.create();
|
||||||
|
tr.insert(0, node);
|
||||||
|
tr.setSelection(Selection.near(tr.doc.resolve(1)));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
ArrowDown: ({ editor }) => {
|
||||||
|
if (!this.options.exitOnArrowDown) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const { state } = editor;
|
const { state } = editor;
|
||||||
const { selection, doc } = state;
|
const { selection, doc } = state;
|
||||||
const { $from, empty } = selection;
|
const { $from, empty } = selection;
|
||||||
@ -18,7 +69,28 @@ export const CustomCodeBlock = CodeBlockLowlight.extend({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return editor.commands.insertContent(" ");
|
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
||||||
|
|
||||||
|
if (!isAtEnd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const after = $from.after();
|
||||||
|
|
||||||
|
if (after === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeAfter = doc.nodeAt(after);
|
||||||
|
|
||||||
|
if (nodeAfter) {
|
||||||
|
return editor.commands.command(({ tr }) => {
|
||||||
|
tr.setSelection(Selection.near(doc.resolve(after)));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor.commands.exitCode();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
import {
|
||||||
|
combineTransactionSteps,
|
||||||
|
findChildrenInRange,
|
||||||
|
getChangedRanges,
|
||||||
|
getMarksBetween,
|
||||||
|
NodeWithPos,
|
||||||
|
} from "@tiptap/core";
|
||||||
|
import { MarkType } from "@tiptap/pm/model";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { find } from "linkifyjs";
|
||||||
|
|
||||||
|
type AutolinkOptions = {
|
||||||
|
type: MarkType;
|
||||||
|
validate?: (url: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function autolink(options: AutolinkOptions): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("autolink"),
|
||||||
|
appendTransaction: (transactions, oldState, newState) => {
|
||||||
|
const docChanges = transactions.some((transaction) => transaction.docChanged) && !oldState.doc.eq(newState.doc);
|
||||||
|
const preventAutolink = transactions.some((transaction) => transaction.getMeta("preventAutolink"));
|
||||||
|
|
||||||
|
if (!docChanges || preventAutolink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tr } = newState;
|
||||||
|
const transform = combineTransactionSteps(oldState.doc, [...transactions]);
|
||||||
|
const changes = getChangedRanges(transform);
|
||||||
|
|
||||||
|
changes.forEach(({ newRange }) => {
|
||||||
|
// Now let’s see if we can add new links.
|
||||||
|
const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, (node) => node.isTextblock);
|
||||||
|
|
||||||
|
let textBlock: NodeWithPos | undefined;
|
||||||
|
let textBeforeWhitespace: string | undefined;
|
||||||
|
|
||||||
|
if (nodesInChangedRanges.length > 1) {
|
||||||
|
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
|
||||||
|
textBlock = nodesInChangedRanges[0];
|
||||||
|
textBeforeWhitespace = newState.doc.textBetween(
|
||||||
|
textBlock.pos,
|
||||||
|
textBlock.pos + textBlock.node.nodeSize,
|
||||||
|
undefined,
|
||||||
|
" "
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
nodesInChangedRanges.length &&
|
||||||
|
// We want to make sure to include the block seperator argument to treat hard breaks like spaces.
|
||||||
|
newState.doc.textBetween(newRange.from, newRange.to, " ", " ").endsWith(" ")
|
||||||
|
) {
|
||||||
|
textBlock = nodesInChangedRanges[0];
|
||||||
|
textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, undefined, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textBlock && textBeforeWhitespace) {
|
||||||
|
const wordsBeforeWhitespace = textBeforeWhitespace.split(" ").filter((s) => s !== "");
|
||||||
|
|
||||||
|
if (wordsBeforeWhitespace.length <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
|
||||||
|
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
|
||||||
|
|
||||||
|
if (!lastWordBeforeSpace) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
find(lastWordBeforeSpace)
|
||||||
|
.filter((link) => link.isLink)
|
||||||
|
// Calculate link position.
|
||||||
|
.map((link) => ({
|
||||||
|
...link,
|
||||||
|
from: lastWordAndBlockOffset + link.start + 1,
|
||||||
|
to: lastWordAndBlockOffset + link.end + 1,
|
||||||
|
}))
|
||||||
|
// ignore link inside code mark
|
||||||
|
.filter((link) => {
|
||||||
|
if (!newState.schema.marks.code) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code);
|
||||||
|
})
|
||||||
|
// validate link
|
||||||
|
.filter((link) => {
|
||||||
|
if (options.validate) {
|
||||||
|
return options.validate(link.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
// Add link mark.
|
||||||
|
.forEach((link) => {
|
||||||
|
if (getMarksBetween(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.addMark(
|
||||||
|
link.from,
|
||||||
|
link.to,
|
||||||
|
options.type.create({
|
||||||
|
href: link.href,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tr.steps.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { getAttributes } from "@tiptap/core";
|
||||||
|
import { MarkType } from "@tiptap/pm/model";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
|
||||||
|
type ClickHandlerOptions = {
|
||||||
|
type: MarkType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("handleClickLink"),
|
||||||
|
props: {
|
||||||
|
handleClick: (view, pos, event) => {
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTarget = event.target as HTMLElement;
|
||||||
|
|
||||||
|
if (eventTarget.nodeName !== "A") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrs = getAttributes(view.state, options.type.name);
|
||||||
|
const link = event.target as HTMLLinkElement;
|
||||||
|
|
||||||
|
const href = link?.href ?? attrs.href;
|
||||||
|
const target = link?.target ?? attrs.target;
|
||||||
|
|
||||||
|
if (link && href) {
|
||||||
|
if (view.editable) {
|
||||||
|
window.open(href, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Editor } from "@tiptap/core";
|
||||||
|
import { MarkType } from "@tiptap/pm/model";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { find } from "linkifyjs";
|
||||||
|
|
||||||
|
type PasteHandlerOptions = {
|
||||||
|
editor: Editor;
|
||||||
|
type: MarkType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pasteHandler(options: PasteHandlerOptions): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("handlePasteLink"),
|
||||||
|
props: {
|
||||||
|
handlePaste: (view, event, slice) => {
|
||||||
|
const { state } = view;
|
||||||
|
const { selection } = state;
|
||||||
|
const { empty } = selection;
|
||||||
|
|
||||||
|
if (empty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textContent = "";
|
||||||
|
|
||||||
|
slice.content.forEach((node) => {
|
||||||
|
textContent += node.textContent;
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = find(textContent).find((item) => item.isLink && item.value === textContent);
|
||||||
|
|
||||||
|
if (!textContent || !link) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = event.clipboardData?.getData("text/html");
|
||||||
|
|
||||||
|
const hrefRegex = /href="([^"]*)"/;
|
||||||
|
|
||||||
|
const existingLink = html?.match(hrefRegex);
|
||||||
|
|
||||||
|
const url = existingLink ? existingLink[1] : link.href;
|
||||||
|
|
||||||
|
options.editor.commands.setMark(options.type, {
|
||||||
|
href: url,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
219
packages/editor/core/src/ui/extensions/custom-link/index.tsx
Normal file
219
packages/editor/core/src/ui/extensions/custom-link/index.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { Plugin } from "@tiptap/pm/state";
|
||||||
|
import { find, registerCustomProtocol, reset } from "linkifyjs";
|
||||||
|
|
||||||
|
import { autolink } from "src/ui/extensions/custom-link/helpers/autolink";
|
||||||
|
import { clickHandler } from "src/ui/extensions/custom-link/helpers/clickHandler";
|
||||||
|
import { pasteHandler } from "src/ui/extensions/custom-link/helpers/pasteHandler";
|
||||||
|
|
||||||
|
export interface LinkProtocolOptions {
|
||||||
|
scheme: string;
|
||||||
|
optionalSlashes?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkOptions {
|
||||||
|
autolink: boolean;
|
||||||
|
inclusive: boolean;
|
||||||
|
protocols: Array<LinkProtocolOptions | string>;
|
||||||
|
openOnClick: boolean;
|
||||||
|
linkOnPaste: boolean;
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
validate?: (url: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
link: {
|
||||||
|
setLink: (attributes: {
|
||||||
|
href: string;
|
||||||
|
target?: string | null;
|
||||||
|
rel?: string | null;
|
||||||
|
class?: string | null;
|
||||||
|
}) => ReturnType;
|
||||||
|
toggleLink: (attributes: {
|
||||||
|
href: string;
|
||||||
|
target?: string | null;
|
||||||
|
rel?: string | null;
|
||||||
|
class?: string | null;
|
||||||
|
}) => ReturnType;
|
||||||
|
unsetLink: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomLinkExtension = Mark.create<LinkOptions>({
|
||||||
|
name: "link",
|
||||||
|
|
||||||
|
priority: 1000,
|
||||||
|
|
||||||
|
keepOnSplit: false,
|
||||||
|
|
||||||
|
onCreate() {
|
||||||
|
this.options.protocols.forEach((protocol) => {
|
||||||
|
if (typeof protocol === "string") {
|
||||||
|
registerCustomProtocol(protocol);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registerCustomProtocol(protocol.scheme, protocol.optionalSlashes);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
inclusive() {
|
||||||
|
return this.options.inclusive;
|
||||||
|
},
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
openOnClick: true,
|
||||||
|
linkOnPaste: true,
|
||||||
|
autolink: true,
|
||||||
|
inclusive: false,
|
||||||
|
protocols: [],
|
||||||
|
HTMLAttributes: {
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer nofollow",
|
||||||
|
class: null,
|
||||||
|
},
|
||||||
|
validate: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
href: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
default: this.options.HTMLAttributes.target,
|
||||||
|
},
|
||||||
|
rel: {
|
||||||
|
default: this.options.HTMLAttributes.rel,
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
default: this.options.HTMLAttributes.class,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "a[href]",
|
||||||
|
getAttrs: (node) => {
|
||||||
|
if (typeof node === "string" || !(node instanceof HTMLElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const href = node.getAttribute("href")?.toLowerCase() || "";
|
||||||
|
if (href.startsWith("javascript:") || href.startsWith("data:") || href.startsWith("vbscript:")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
const href = HTMLAttributes.href?.toLowerCase() || "";
|
||||||
|
if (href.startsWith("javascript:") || href.startsWith("data:") || href.startsWith("vbscript:")) {
|
||||||
|
return ["a", mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0];
|
||||||
|
}
|
||||||
|
return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setLink:
|
||||||
|
(attributes) =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain().setMark(this.name, attributes).setMeta("preventAutolink", true).run(),
|
||||||
|
|
||||||
|
toggleLink:
|
||||||
|
(attributes) =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain()
|
||||||
|
.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
|
||||||
|
.setMeta("preventAutolink", true)
|
||||||
|
.run(),
|
||||||
|
|
||||||
|
unsetLink:
|
||||||
|
() =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addPasteRules() {
|
||||||
|
return [
|
||||||
|
markPasteRule({
|
||||||
|
find: (text) =>
|
||||||
|
find(text)
|
||||||
|
.filter((link) => {
|
||||||
|
if (this.options.validate) {
|
||||||
|
return this.options.validate(link.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.filter((link) => link.isLink)
|
||||||
|
.map((link) => ({
|
||||||
|
text: link.value,
|
||||||
|
index: link.start,
|
||||||
|
data: link,
|
||||||
|
})),
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match, pasteEvent) => {
|
||||||
|
const html = pasteEvent?.clipboardData?.getData("text/html");
|
||||||
|
const hrefRegex = /href="([^"]*)"/;
|
||||||
|
|
||||||
|
const existingLink = html?.match(hrefRegex);
|
||||||
|
|
||||||
|
if (existingLink) {
|
||||||
|
return {
|
||||||
|
href: existingLink[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
href: match.data?.href,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const plugins: Plugin[] = [];
|
||||||
|
|
||||||
|
if (this.options.autolink) {
|
||||||
|
plugins.push(
|
||||||
|
autolink({
|
||||||
|
type: this.type,
|
||||||
|
validate: this.options.validate,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.openOnClick) {
|
||||||
|
plugins.push(
|
||||||
|
clickHandler({
|
||||||
|
type: this.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.linkOnPaste) {
|
||||||
|
plugins.push(
|
||||||
|
pasteHandler({
|
||||||
|
editor: this.editor,
|
||||||
|
type: this.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
},
|
||||||
|
});
|
@ -1,5 +1,4 @@
|
|||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import TiptapLink from "@tiptap/extension-link";
|
|
||||||
import TiptapUnderline from "@tiptap/extension-underline";
|
import TiptapUnderline from "@tiptap/extension-underline";
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
import TextStyle from "@tiptap/extension-text-style";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
@ -19,13 +18,15 @@ import { isValidHttpUrl } from "src/lib/utils";
|
|||||||
import { Mentions } from "src/ui/mentions";
|
import { Mentions } from "src/ui/mentions";
|
||||||
|
|
||||||
import { CustomKeymap } from "src/ui/extensions/keymap";
|
import { CustomKeymap } from "src/ui/extensions/keymap";
|
||||||
import { CustomCodeBlock } from "src/ui/extensions/code";
|
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
|
||||||
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
||||||
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
||||||
|
|
||||||
import { DeleteImage } from "src/types/delete-image";
|
import { DeleteImage } from "src/types/delete-image";
|
||||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||||
import { RestoreImage } from "src/types/restore-image";
|
import { RestoreImage } from "src/types/restore-image";
|
||||||
|
import { CustomLinkExtension } from "src/ui/extensions/custom-link";
|
||||||
|
import { CustomCodeInlineExtension } from "./code-inline";
|
||||||
|
|
||||||
export const CoreEditorExtensions = (
|
export const CoreEditorExtensions = (
|
||||||
mentionConfig: {
|
mentionConfig: {
|
||||||
@ -52,12 +53,7 @@ export const CoreEditorExtensions = (
|
|||||||
class: "leading-normal -mb-2",
|
class: "leading-normal -mb-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
code: {
|
code: false,
|
||||||
HTMLAttributes: {
|
|
||||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
|
||||||
spellcheck: "false",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
codeBlock: false,
|
codeBlock: false,
|
||||||
horizontalRule: false,
|
horizontalRule: false,
|
||||||
dropcursor: {
|
dropcursor: {
|
||||||
@ -70,10 +66,12 @@ export const CoreEditorExtensions = (
|
|||||||
}),
|
}),
|
||||||
CustomKeymap,
|
CustomKeymap,
|
||||||
ListKeymap,
|
ListKeymap,
|
||||||
TiptapLink.configure({
|
CustomLinkExtension.configure({
|
||||||
autolink: false,
|
openOnClick: true,
|
||||||
|
autolink: true,
|
||||||
|
linkOnPaste: true,
|
||||||
protocols: ["http", "https"],
|
protocols: ["http", "https"],
|
||||||
validate: (url) => isValidHttpUrl(url),
|
validate: (url: string) => isValidHttpUrl(url),
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class:
|
class:
|
||||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||||
@ -92,13 +90,14 @@ export const CoreEditorExtensions = (
|
|||||||
class: "not-prose pl-2",
|
class: "not-prose pl-2",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
CustomCodeBlock,
|
|
||||||
TaskItem.configure({
|
TaskItem.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "flex items-start my-4",
|
class: "flex items-start my-4",
|
||||||
},
|
},
|
||||||
nested: true,
|
nested: true,
|
||||||
}),
|
}),
|
||||||
|
CustomCodeBlockExtension,
|
||||||
|
CustomCodeInlineExtension,
|
||||||
Markdown.configure({
|
Markdown.configure({
|
||||||
html: true,
|
html: true,
|
||||||
transformCopiedText: true,
|
transformCopiedText: true,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import TiptapLink from "@tiptap/extension-link";
|
|
||||||
import TiptapUnderline from "@tiptap/extension-underline";
|
import TiptapUnderline from "@tiptap/extension-underline";
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
import TextStyle from "@tiptap/extension-text-style";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
@ -18,6 +17,7 @@ import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image"
|
|||||||
import { isValidHttpUrl } from "src/lib/utils";
|
import { isValidHttpUrl } from "src/lib/utils";
|
||||||
import { Mentions } from "src/ui/mentions";
|
import { Mentions } from "src/ui/mentions";
|
||||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||||
|
import { CustomLinkExtension } from "src/ui/extensions/custom-link";
|
||||||
|
|
||||||
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||||
mentionSuggestions: IMentionSuggestion[];
|
mentionSuggestions: IMentionSuggestion[];
|
||||||
@ -59,7 +59,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
|||||||
gapcursor: false,
|
gapcursor: false,
|
||||||
}),
|
}),
|
||||||
Gapcursor,
|
Gapcursor,
|
||||||
TiptapLink.configure({
|
CustomLinkExtension.configure({
|
||||||
protocols: ["http", "https"],
|
protocols: ["http", "https"],
|
||||||
validate: (url) => isValidHttpUrl(url),
|
validate: (url) => isValidHttpUrl(url),
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -65,12 +65,16 @@ export class FileService extends APIService {
|
|||||||
|
|
||||||
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||||
return async (file: File) => {
|
return async (file: File) => {
|
||||||
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("asset", file);
|
formData.append("asset", file);
|
||||||
formData.append("attributes", JSON.stringify({}));
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
|
||||||
const data = await this.uploadFile(workspaceSlug, formData);
|
const data = await this.uploadFile(workspaceSlug, formData);
|
||||||
return data.asset;
|
return data.asset;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
yarn.lock
11
yarn.lock
@ -2492,13 +2492,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.13.tgz#1e9521dea002c8d6de833d9fd928d4617623eab8"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.13.tgz#1e9521dea002c8d6de833d9fd928d4617623eab8"
|
||||||
integrity sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==
|
integrity sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==
|
||||||
|
|
||||||
"@tiptap/extension-link@^2.1.13":
|
|
||||||
version "2.1.13"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.1.13.tgz#ae4abd7c43292e3a1841488bfc7a687b2f014249"
|
|
||||||
integrity sha512-wuGMf3zRtMHhMrKm9l6Tft5M2N21Z0UP1dZ5t1IlOAvOeYV2QZ5UynwFryxGKLO0NslCBLF/4b/HAdNXbfXWUA==
|
|
||||||
dependencies:
|
|
||||||
linkifyjs "^4.1.0"
|
|
||||||
|
|
||||||
"@tiptap/extension-list-item@^2.1.13":
|
"@tiptap/extension-list-item@^2.1.13":
|
||||||
version "2.1.13"
|
version "2.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.13.tgz#3c62127df97974f3196866ec00ee397f4c9acdc4"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.13.tgz#3c62127df97974f3196866ec00ee397f4c9acdc4"
|
||||||
@ -2802,7 +2795,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
|
"@types/react@*", "@types/react@^18.2.42":
|
||||||
version "18.2.42"
|
version "18.2.42"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
||||||
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
||||||
@ -6071,7 +6064,7 @@ linkify-it@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
uc.micro "^2.0.0"
|
uc.micro "^2.0.0"
|
||||||
|
|
||||||
linkifyjs@^4.1.0:
|
linkifyjs@^4.1.3:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f"
|
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f"
|
||||||
integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==
|
integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==
|
||||||
|
Loading…
Reference in New Issue
Block a user