-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
3a02b80
commit 4dfe1ef
Showing
12 changed files
with
568 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
packages/editor/core/src/ui/extensions/code-inline/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
packages/editor/core/src/ui/extensions/custom-link/helpers/autolink.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}, | ||
}); | ||
} |
42 changes: 42 additions & 0 deletions
42
packages/editor/core/src/ui/extensions/custom-link/helpers/clickHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}, | ||
}, | ||
}); | ||
} |
52 changes: 52 additions & 0 deletions
52
packages/editor/core/src/ui/extensions/custom-link/helpers/pasteHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}, | ||
}, | ||
}); | ||
} |
Oops, something went wrong.