@tiptap/extension-link 2.0.0-beta.29 → 2.0.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import { MarkType } from 'prosemirror-model';
3
+ declare type AutolinkOptions = {
4
+ type: MarkType;
5
+ };
6
+ export default function autolink(options: AutolinkOptions): Plugin;
7
+ export {};
@@ -0,0 +1,7 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import { MarkType } from 'prosemirror-model';
3
+ declare type ClickHandlerOptions = {
4
+ type: MarkType;
5
+ };
6
+ export default function clickHandler(options: ClickHandlerOptions): Plugin;
7
+ export {};
@@ -0,0 +1,9 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import { Plugin } from 'prosemirror-state';
3
+ import { MarkType } from 'prosemirror-model';
4
+ declare type PasteHandlerOptions = {
5
+ editor: Editor;
6
+ type: MarkType;
7
+ };
8
+ export default function pasteHandler(options: PasteHandlerOptions): Plugin;
9
+ export {};
@@ -1,5 +1,9 @@
1
1
  import { Mark } from '@tiptap/core';
2
2
  export interface LinkOptions {
3
+ /**
4
+ * If enabled, it adds links as you type.
5
+ */
6
+ autolink: boolean;
3
7
  /**
4
8
  * If enabled, links will be opened on click.
5
9
  */
@@ -3,17 +3,139 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var core = require('@tiptap/core');
6
- var prosemirrorState = require('prosemirror-state');
7
6
  var linkifyjs = require('linkifyjs');
7
+ var prosemirrorState = require('prosemirror-state');
8
+
9
+ function autolink(options) {
10
+ return new prosemirrorState.Plugin({
11
+ key: new prosemirrorState.PluginKey('autolink'),
12
+ appendTransaction: (transactions, oldState, newState) => {
13
+ const docChanges = transactions.some(transaction => transaction.docChanged)
14
+ && !oldState.doc.eq(newState.doc);
15
+ if (!docChanges) {
16
+ return;
17
+ }
18
+ const { tr } = newState;
19
+ const transform = core.combineTransactionSteps(oldState.doc, transactions);
20
+ const { mapping } = transform;
21
+ const changes = core.getChangedRanges(transform);
22
+ changes.forEach(({ oldRange, newRange }) => {
23
+ // at first we check if we have to remove links
24
+ core.getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
25
+ .filter(item => item.mark.type === options.type)
26
+ .forEach(oldMark => {
27
+ const newFrom = mapping.map(oldMark.from);
28
+ const newTo = mapping.map(oldMark.to);
29
+ const newMarks = core.getMarksBetween(newFrom, newTo, newState.doc)
30
+ .filter(item => item.mark.type === options.type);
31
+ if (!newMarks.length) {
32
+ return;
33
+ }
34
+ const newMark = newMarks[0];
35
+ const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ');
36
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ');
37
+ const wasLink = linkifyjs.test(oldLinkText);
38
+ const isLink = linkifyjs.test(newLinkText);
39
+ // remove only the link, if it was a link before too
40
+ // because we don’t want to remove links that were set manually
41
+ if (wasLink && !isLink) {
42
+ tr.removeMark(newMark.from, newMark.to, options.type);
43
+ }
44
+ });
45
+ // now let’s see if we can add new links
46
+ core.findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
47
+ .forEach(textBlock => {
48
+ // we need to define a placeholder for leaf nodes
49
+ // so that the link position can be calculated correctly
50
+ const text = newState.doc.textBetween(textBlock.pos, textBlock.pos + textBlock.node.nodeSize, undefined, ' ');
51
+ linkifyjs.find(text)
52
+ .filter(link => link.isLink)
53
+ // calculate link position
54
+ .map(link => ({
55
+ ...link,
56
+ from: textBlock.pos + link.start + 1,
57
+ to: textBlock.pos + link.end + 1,
58
+ }))
59
+ // check if link is within the changed range
60
+ .filter(link => {
61
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
62
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
63
+ return fromIsInRange || toIsInRange;
64
+ })
65
+ // add link mark
66
+ .forEach(link => {
67
+ tr.addMark(link.from, link.to, options.type.create({
68
+ href: link.href,
69
+ }));
70
+ });
71
+ });
72
+ });
73
+ if (!tr.steps.length) {
74
+ return;
75
+ }
76
+ return tr;
77
+ },
78
+ });
79
+ }
80
+
81
+ function clickHandler(options) {
82
+ return new prosemirrorState.Plugin({
83
+ key: new prosemirrorState.PluginKey('handleClickLink'),
84
+ props: {
85
+ handleClick: (view, pos, event) => {
86
+ var _a;
87
+ const attrs = core.getAttributes(view.state, options.type.name);
88
+ const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
89
+ if (link && attrs.href) {
90
+ window.open(attrs.href, attrs.target);
91
+ return true;
92
+ }
93
+ return false;
94
+ },
95
+ },
96
+ });
97
+ }
98
+
99
+ function pasteHandler(options) {
100
+ return new prosemirrorState.Plugin({
101
+ key: new prosemirrorState.PluginKey('handlePasteLink'),
102
+ props: {
103
+ handlePaste: (view, event, slice) => {
104
+ const { state } = view;
105
+ const { selection } = state;
106
+ const { empty } = selection;
107
+ if (empty) {
108
+ return false;
109
+ }
110
+ let textContent = '';
111
+ slice.content.forEach(node => {
112
+ textContent += node.textContent;
113
+ });
114
+ const link = linkifyjs.find(textContent).find(item => item.isLink && item.value === textContent);
115
+ if (!textContent || !link) {
116
+ return false;
117
+ }
118
+ options.editor.commands.setMark(options.type, {
119
+ href: link.href,
120
+ });
121
+ return true;
122
+ },
123
+ },
124
+ });
125
+ }
8
126
 
9
127
  const Link = core.Mark.create({
10
128
  name: 'link',
11
129
  priority: 1000,
12
- inclusive: false,
130
+ keepOnSplit: false,
131
+ inclusive() {
132
+ return this.options.autolink;
133
+ },
13
134
  addOptions() {
14
135
  return {
15
136
  openOnClick: true,
16
137
  linkOnPaste: true,
138
+ autolink: true,
17
139
  HTMLAttributes: {
18
140
  target: '_blank',
19
141
  rel: 'noopener noreferrer nofollow',
@@ -36,7 +158,11 @@ const Link = core.Mark.create({
36
158
  ];
37
159
  },
38
160
  renderHTML({ HTMLAttributes }) {
39
- return ['a', core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
161
+ return [
162
+ 'a',
163
+ core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
164
+ 0,
165
+ ];
40
166
  },
41
167
  addCommands() {
42
168
  return {
@@ -73,49 +199,20 @@ const Link = core.Mark.create({
73
199
  },
74
200
  addProseMirrorPlugins() {
75
201
  const plugins = [];
202
+ if (this.options.autolink) {
203
+ plugins.push(autolink({
204
+ type: this.type,
205
+ }));
206
+ }
76
207
  if (this.options.openOnClick) {
77
- plugins.push(new prosemirrorState.Plugin({
78
- key: new prosemirrorState.PluginKey('handleClickLink'),
79
- props: {
80
- handleClick: (view, pos, event) => {
81
- var _a;
82
- const attrs = this.editor.getAttributes(this.name);
83
- const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
84
- if (link && attrs.href) {
85
- window.open(attrs.href, attrs.target);
86
- return true;
87
- }
88
- return false;
89
- },
90
- },
208
+ plugins.push(clickHandler({
209
+ type: this.type,
91
210
  }));
92
211
  }
93
212
  if (this.options.linkOnPaste) {
94
- plugins.push(new prosemirrorState.Plugin({
95
- key: new prosemirrorState.PluginKey('handlePasteLink'),
96
- props: {
97
- handlePaste: (view, event, slice) => {
98
- const { state } = view;
99
- const { selection } = state;
100
- const { empty } = selection;
101
- if (empty) {
102
- return false;
103
- }
104
- let textContent = '';
105
- slice.content.forEach(node => {
106
- textContent += node.textContent;
107
- });
108
- const link = linkifyjs.find(textContent)
109
- .find(item => item.isLink && item.value === textContent);
110
- if (!textContent || !link) {
111
- return false;
112
- }
113
- this.editor.commands.setMark(this.type, {
114
- href: link.href,
115
- });
116
- return true;
117
- },
118
- },
213
+ plugins.push(pasteHandler({
214
+ editor: this.editor,
215
+ type: this.type,
119
216
  }));
120
217
  }
121
218
  return plugins;
@@ -1 +1 @@
1
- {"version":3,"file":"tiptap-extension-link.cjs.js","sources":["../src/link.ts"],"sourcesContent":["import {\n Mark,\n markPasteRule,\n mergeAttributes,\n} from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { find } from 'linkifyjs'\n\nexport interface LinkOptions {\n /**\n * If enabled, links will be opened on click.\n */\n openOnClick: boolean,\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n */\n linkOnPaste: boolean,\n /**\n * A list of HTML attributes to be rendered.\n */\n HTMLAttributes: Record<string, any>,\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n */\n setLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Toggle a link mark\n */\n toggleLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Unset a link mark\n */\n unsetLink: () => ReturnType,\n }\n }\n}\n\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n inclusive: false,\n\n addOptions() {\n return {\n openOnClick: true,\n linkOnPaste: true,\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n },\n target: {\n default: this.options.HTMLAttributes.target,\n },\n }\n },\n\n parseHTML() {\n return [\n { tag: 'a[href]' },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n addCommands() {\n return {\n setLink: attributes => ({ commands }) => {\n return commands.setMark(this.name, attributes)\n },\n toggleLink: attributes => ({ commands }) => {\n return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n },\n unsetLink: () => ({ commands }) => {\n return commands.unsetMark(this.name, { extendEmptyMarkRange: true })\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => find(text)\n .filter(link => link.isLink)\n .map(link => ({\n text: link.value,\n index: link.start,\n data: link,\n })),\n type: this.type,\n getAttributes: match => ({\n href: match.data?.href,\n }),\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins = []\n\n if (this.options.openOnClick) {\n plugins.push(\n new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n const attrs = this.editor.getAttributes(this.name)\n const link = (event.target as HTMLElement)?.closest('a')\n\n if (link && attrs.href) {\n window.open(attrs.href, attrs.target)\n\n return true\n }\n\n return false\n },\n },\n }),\n )\n }\n\n if (this.options.linkOnPaste) {\n plugins.push(\n new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, event, slice) => {\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent)\n .find(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n this.editor.commands.setMark(this.type, {\n href: link.href,\n })\n\n return true\n },\n },\n }),\n )\n }\n\n return plugins\n },\n})\n"],"names":["Mark","mergeAttributes","markPasteRule","find","Plugin","PluginKey"],"mappings":"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"}
1
+ {"version":3,"file":"tiptap-extension-link.cjs.js","sources":["../src/helpers/autolink.ts","../src/helpers/clickHandler.ts","../src/helpers/pasteHandler.ts","../src/link.ts"],"sourcesContent":["import {\n getMarksBetween,\n findChildrenInRange,\n combineTransactionSteps,\n getChangedRanges,\n} from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\nimport { find, test } from 'linkifyjs'\n\ntype AutolinkOptions = {\n type: MarkType,\n}\n\nexport default function autolink(options: AutolinkOptions): Plugin {\n return new Plugin({\n key: new PluginKey('autolink'),\n appendTransaction: (transactions, oldState, newState) => {\n const docChanges = transactions.some(transaction => transaction.docChanged)\n && !oldState.doc.eq(newState.doc)\n\n if (!docChanges) {\n return\n }\n\n const { tr } = newState\n const transform = combineTransactionSteps(oldState.doc, transactions)\n const { mapping } = transform\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ oldRange, newRange }) => {\n // at first we check if we have to remove links\n getMarksBetween(oldRange.from, oldRange.to, oldState.doc)\n .filter(item => item.mark.type === options.type)\n .forEach(oldMark => {\n const newFrom = mapping.map(oldMark.from)\n const newTo = mapping.map(oldMark.to)\n const newMarks = getMarksBetween(newFrom, newTo, newState.doc)\n .filter(item => item.mark.type === options.type)\n\n if (!newMarks.length) {\n return\n }\n\n const newMark = newMarks[0]\n const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ')\n const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ')\n const wasLink = test(oldLinkText)\n const isLink = test(newLinkText)\n\n // remove only the link, if it was a link before too\n // because we don’t want to remove links that were set manually\n if (wasLink && !isLink) {\n tr.removeMark(newMark.from, newMark.to, options.type)\n }\n })\n\n // now let’s see if we can add new links\n findChildrenInRange(newState.doc, newRange, node => node.isTextblock)\n .forEach(textBlock => {\n // we need to define a placeholder for leaf nodes\n // so that the link position can be calculated correctly\n const text = newState.doc.textBetween(\n textBlock.pos,\n textBlock.pos + textBlock.node.nodeSize,\n undefined,\n ' ',\n )\n\n find(text)\n .filter(link => link.isLink)\n // calculate link position\n .map(link => ({\n ...link,\n from: textBlock.pos + link.start + 1,\n to: textBlock.pos + link.end + 1,\n }))\n // check if link is within the changed range\n .filter(link => {\n const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to\n const toIsInRange = newRange.to >= link.from && newRange.to <= link.to\n\n return fromIsInRange || toIsInRange\n })\n // add link mark\n .forEach(link => {\n tr.addMark(link.from, link.to, options.type.create({\n href: link.href,\n }))\n })\n })\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n })\n}\n","import { getAttributes } from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\n\ntype ClickHandlerOptions = {\n type: MarkType,\n}\n\nexport default function clickHandler(options: ClickHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n const attrs = getAttributes(view.state, options.type.name)\n const link = (event.target as HTMLElement)?.closest('a')\n\n if (link && attrs.href) {\n window.open(attrs.href, attrs.target)\n\n return true\n }\n\n return false\n },\n },\n })\n}\n","import { Editor } from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\nimport { find } from 'linkifyjs'\n\ntype PasteHandlerOptions = {\n editor: Editor,\n type: MarkType,\n}\n\nexport default function pasteHandler(options: PasteHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, event, slice) => {\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent).find(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n\n return true\n },\n },\n })\n}\n","import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'\nimport { find } from 'linkifyjs'\nimport autolink from './helpers/autolink'\nimport clickHandler from './helpers/clickHandler'\nimport pasteHandler from './helpers/pasteHandler'\n\nexport interface LinkOptions {\n /**\n * If enabled, it adds links as you type.\n */\n autolink: boolean,\n /**\n * If enabled, links will be opened on click.\n */\n openOnClick: boolean,\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n */\n linkOnPaste: boolean,\n /**\n * A list of HTML attributes to be rendered.\n */\n HTMLAttributes: Record<string, any>,\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n */\n setLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Toggle a link mark\n */\n toggleLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Unset a link mark\n */\n unsetLink: () => ReturnType,\n }\n }\n}\n\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n keepOnSplit: false,\n\n inclusive() {\n return this.options.autolink\n },\n\n addOptions() {\n return {\n openOnClick: true,\n linkOnPaste: true,\n autolink: true,\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n },\n target: {\n default: this.options.HTMLAttributes.target,\n },\n }\n },\n\n parseHTML() {\n return [\n { tag: 'a[href]' },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return [\n 'a',\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n 0,\n ]\n },\n\n addCommands() {\n return {\n setLink: attributes => ({ commands }) => {\n return commands.setMark(this.name, attributes)\n },\n\n toggleLink: attributes => ({ commands }) => {\n return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n },\n\n unsetLink: () => ({ commands }) => {\n return commands.unsetMark(this.name, { extendEmptyMarkRange: true })\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => find(text)\n .filter(link => link.isLink)\n .map(link => ({\n text: link.value,\n index: link.start,\n data: link,\n })),\n type: this.type,\n getAttributes: match => ({\n href: match.data?.href,\n }),\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins = []\n\n if (this.options.autolink) {\n plugins.push(autolink({\n type: this.type,\n }))\n }\n\n if (this.options.openOnClick) {\n plugins.push(clickHandler({\n type: this.type,\n }))\n }\n\n if (this.options.linkOnPaste) {\n plugins.push(pasteHandler({\n editor: this.editor,\n type: this.type,\n }))\n }\n\n return plugins\n },\n})\n"],"names":["Plugin","PluginKey","combineTransactionSteps","getChangedRanges","getMarksBetween","test","findChildrenInRange","find","getAttributes","Mark","mergeAttributes","markPasteRule"],"mappings":"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"}
@@ -1,15 +1,137 @@
1
- import { Mark, mergeAttributes, markPasteRule } from '@tiptap/core';
1
+ import { combineTransactionSteps, getChangedRanges, getMarksBetween, findChildrenInRange, getAttributes, Mark, mergeAttributes, markPasteRule } from '@tiptap/core';
2
+ import { test, find } from 'linkifyjs';
2
3
  import { Plugin, PluginKey } from 'prosemirror-state';
3
- import { find } from 'linkifyjs';
4
+
5
+ function autolink(options) {
6
+ return new Plugin({
7
+ key: new PluginKey('autolink'),
8
+ appendTransaction: (transactions, oldState, newState) => {
9
+ const docChanges = transactions.some(transaction => transaction.docChanged)
10
+ && !oldState.doc.eq(newState.doc);
11
+ if (!docChanges) {
12
+ return;
13
+ }
14
+ const { tr } = newState;
15
+ const transform = combineTransactionSteps(oldState.doc, transactions);
16
+ const { mapping } = transform;
17
+ const changes = getChangedRanges(transform);
18
+ changes.forEach(({ oldRange, newRange }) => {
19
+ // at first we check if we have to remove links
20
+ getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
21
+ .filter(item => item.mark.type === options.type)
22
+ .forEach(oldMark => {
23
+ const newFrom = mapping.map(oldMark.from);
24
+ const newTo = mapping.map(oldMark.to);
25
+ const newMarks = getMarksBetween(newFrom, newTo, newState.doc)
26
+ .filter(item => item.mark.type === options.type);
27
+ if (!newMarks.length) {
28
+ return;
29
+ }
30
+ const newMark = newMarks[0];
31
+ const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ');
32
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ');
33
+ const wasLink = test(oldLinkText);
34
+ const isLink = test(newLinkText);
35
+ // remove only the link, if it was a link before too
36
+ // because we don’t want to remove links that were set manually
37
+ if (wasLink && !isLink) {
38
+ tr.removeMark(newMark.from, newMark.to, options.type);
39
+ }
40
+ });
41
+ // now let’s see if we can add new links
42
+ findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
43
+ .forEach(textBlock => {
44
+ // we need to define a placeholder for leaf nodes
45
+ // so that the link position can be calculated correctly
46
+ const text = newState.doc.textBetween(textBlock.pos, textBlock.pos + textBlock.node.nodeSize, undefined, ' ');
47
+ find(text)
48
+ .filter(link => link.isLink)
49
+ // calculate link position
50
+ .map(link => ({
51
+ ...link,
52
+ from: textBlock.pos + link.start + 1,
53
+ to: textBlock.pos + link.end + 1,
54
+ }))
55
+ // check if link is within the changed range
56
+ .filter(link => {
57
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
58
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
59
+ return fromIsInRange || toIsInRange;
60
+ })
61
+ // add link mark
62
+ .forEach(link => {
63
+ tr.addMark(link.from, link.to, options.type.create({
64
+ href: link.href,
65
+ }));
66
+ });
67
+ });
68
+ });
69
+ if (!tr.steps.length) {
70
+ return;
71
+ }
72
+ return tr;
73
+ },
74
+ });
75
+ }
76
+
77
+ function clickHandler(options) {
78
+ return new Plugin({
79
+ key: new PluginKey('handleClickLink'),
80
+ props: {
81
+ handleClick: (view, pos, event) => {
82
+ var _a;
83
+ const attrs = getAttributes(view.state, options.type.name);
84
+ const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
85
+ if (link && attrs.href) {
86
+ window.open(attrs.href, attrs.target);
87
+ return true;
88
+ }
89
+ return false;
90
+ },
91
+ },
92
+ });
93
+ }
94
+
95
+ function pasteHandler(options) {
96
+ return new Plugin({
97
+ key: new PluginKey('handlePasteLink'),
98
+ props: {
99
+ handlePaste: (view, event, slice) => {
100
+ const { state } = view;
101
+ const { selection } = state;
102
+ const { empty } = selection;
103
+ if (empty) {
104
+ return false;
105
+ }
106
+ let textContent = '';
107
+ slice.content.forEach(node => {
108
+ textContent += node.textContent;
109
+ });
110
+ const link = find(textContent).find(item => item.isLink && item.value === textContent);
111
+ if (!textContent || !link) {
112
+ return false;
113
+ }
114
+ options.editor.commands.setMark(options.type, {
115
+ href: link.href,
116
+ });
117
+ return true;
118
+ },
119
+ },
120
+ });
121
+ }
4
122
 
5
123
  const Link = Mark.create({
6
124
  name: 'link',
7
125
  priority: 1000,
8
- inclusive: false,
126
+ keepOnSplit: false,
127
+ inclusive() {
128
+ return this.options.autolink;
129
+ },
9
130
  addOptions() {
10
131
  return {
11
132
  openOnClick: true,
12
133
  linkOnPaste: true,
134
+ autolink: true,
13
135
  HTMLAttributes: {
14
136
  target: '_blank',
15
137
  rel: 'noopener noreferrer nofollow',
@@ -32,7 +154,11 @@ const Link = Mark.create({
32
154
  ];
33
155
  },
34
156
  renderHTML({ HTMLAttributes }) {
35
- return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
157
+ return [
158
+ 'a',
159
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
160
+ 0,
161
+ ];
36
162
  },
37
163
  addCommands() {
38
164
  return {
@@ -69,49 +195,20 @@ const Link = Mark.create({
69
195
  },
70
196
  addProseMirrorPlugins() {
71
197
  const plugins = [];
198
+ if (this.options.autolink) {
199
+ plugins.push(autolink({
200
+ type: this.type,
201
+ }));
202
+ }
72
203
  if (this.options.openOnClick) {
73
- plugins.push(new Plugin({
74
- key: new PluginKey('handleClickLink'),
75
- props: {
76
- handleClick: (view, pos, event) => {
77
- var _a;
78
- const attrs = this.editor.getAttributes(this.name);
79
- const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
80
- if (link && attrs.href) {
81
- window.open(attrs.href, attrs.target);
82
- return true;
83
- }
84
- return false;
85
- },
86
- },
204
+ plugins.push(clickHandler({
205
+ type: this.type,
87
206
  }));
88
207
  }
89
208
  if (this.options.linkOnPaste) {
90
- plugins.push(new Plugin({
91
- key: new PluginKey('handlePasteLink'),
92
- props: {
93
- handlePaste: (view, event, slice) => {
94
- const { state } = view;
95
- const { selection } = state;
96
- const { empty } = selection;
97
- if (empty) {
98
- return false;
99
- }
100
- let textContent = '';
101
- slice.content.forEach(node => {
102
- textContent += node.textContent;
103
- });
104
- const link = find(textContent)
105
- .find(item => item.isLink && item.value === textContent);
106
- if (!textContent || !link) {
107
- return false;
108
- }
109
- this.editor.commands.setMark(this.type, {
110
- href: link.href,
111
- });
112
- return true;
113
- },
114
- },
209
+ plugins.push(pasteHandler({
210
+ editor: this.editor,
211
+ type: this.type,
115
212
  }));
116
213
  }
117
214
  return plugins;
@@ -1 +1 @@
1
- {"version":3,"file":"tiptap-extension-link.esm.js","sources":["../src/link.ts"],"sourcesContent":["import {\n Mark,\n markPasteRule,\n mergeAttributes,\n} from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { find } from 'linkifyjs'\n\nexport interface LinkOptions {\n /**\n * If enabled, links will be opened on click.\n */\n openOnClick: boolean,\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n */\n linkOnPaste: boolean,\n /**\n * A list of HTML attributes to be rendered.\n */\n HTMLAttributes: Record<string, any>,\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n */\n setLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Toggle a link mark\n */\n toggleLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Unset a link mark\n */\n unsetLink: () => ReturnType,\n }\n }\n}\n\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n inclusive: false,\n\n addOptions() {\n return {\n openOnClick: true,\n linkOnPaste: true,\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n },\n target: {\n default: this.options.HTMLAttributes.target,\n },\n }\n },\n\n parseHTML() {\n return [\n { tag: 'a[href]' },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n addCommands() {\n return {\n setLink: attributes => ({ commands }) => {\n return commands.setMark(this.name, attributes)\n },\n toggleLink: attributes => ({ commands }) => {\n return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n },\n unsetLink: () => ({ commands }) => {\n return commands.unsetMark(this.name, { extendEmptyMarkRange: true })\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => find(text)\n .filter(link => link.isLink)\n .map(link => ({\n text: link.value,\n index: link.start,\n data: link,\n })),\n type: this.type,\n getAttributes: match => ({\n href: match.data?.href,\n }),\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins = []\n\n if (this.options.openOnClick) {\n plugins.push(\n new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n const attrs = this.editor.getAttributes(this.name)\n const link = (event.target as HTMLElement)?.closest('a')\n\n if (link && attrs.href) {\n window.open(attrs.href, attrs.target)\n\n return true\n }\n\n return false\n },\n },\n }),\n )\n }\n\n if (this.options.linkOnPaste) {\n plugins.push(\n new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, event, slice) => {\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent)\n .find(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n this.editor.commands.setMark(this.type, {\n href: link.href,\n })\n\n return true\n },\n },\n }),\n )\n }\n\n return plugins\n },\n})\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tiptap-extension-link.esm.js","sources":["../src/helpers/autolink.ts","../src/helpers/clickHandler.ts","../src/helpers/pasteHandler.ts","../src/link.ts"],"sourcesContent":["import {\n getMarksBetween,\n findChildrenInRange,\n combineTransactionSteps,\n getChangedRanges,\n} from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\nimport { find, test } from 'linkifyjs'\n\ntype AutolinkOptions = {\n type: MarkType,\n}\n\nexport default function autolink(options: AutolinkOptions): Plugin {\n return new Plugin({\n key: new PluginKey('autolink'),\n appendTransaction: (transactions, oldState, newState) => {\n const docChanges = transactions.some(transaction => transaction.docChanged)\n && !oldState.doc.eq(newState.doc)\n\n if (!docChanges) {\n return\n }\n\n const { tr } = newState\n const transform = combineTransactionSteps(oldState.doc, transactions)\n const { mapping } = transform\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ oldRange, newRange }) => {\n // at first we check if we have to remove links\n getMarksBetween(oldRange.from, oldRange.to, oldState.doc)\n .filter(item => item.mark.type === options.type)\n .forEach(oldMark => {\n const newFrom = mapping.map(oldMark.from)\n const newTo = mapping.map(oldMark.to)\n const newMarks = getMarksBetween(newFrom, newTo, newState.doc)\n .filter(item => item.mark.type === options.type)\n\n if (!newMarks.length) {\n return\n }\n\n const newMark = newMarks[0]\n const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ')\n const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ')\n const wasLink = test(oldLinkText)\n const isLink = test(newLinkText)\n\n // remove only the link, if it was a link before too\n // because we don’t want to remove links that were set manually\n if (wasLink && !isLink) {\n tr.removeMark(newMark.from, newMark.to, options.type)\n }\n })\n\n // now let’s see if we can add new links\n findChildrenInRange(newState.doc, newRange, node => node.isTextblock)\n .forEach(textBlock => {\n // we need to define a placeholder for leaf nodes\n // so that the link position can be calculated correctly\n const text = newState.doc.textBetween(\n textBlock.pos,\n textBlock.pos + textBlock.node.nodeSize,\n undefined,\n ' ',\n )\n\n find(text)\n .filter(link => link.isLink)\n // calculate link position\n .map(link => ({\n ...link,\n from: textBlock.pos + link.start + 1,\n to: textBlock.pos + link.end + 1,\n }))\n // check if link is within the changed range\n .filter(link => {\n const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to\n const toIsInRange = newRange.to >= link.from && newRange.to <= link.to\n\n return fromIsInRange || toIsInRange\n })\n // add link mark\n .forEach(link => {\n tr.addMark(link.from, link.to, options.type.create({\n href: link.href,\n }))\n })\n })\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n })\n}\n","import { getAttributes } from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\n\ntype ClickHandlerOptions = {\n type: MarkType,\n}\n\nexport default function clickHandler(options: ClickHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n const attrs = getAttributes(view.state, options.type.name)\n const link = (event.target as HTMLElement)?.closest('a')\n\n if (link && attrs.href) {\n window.open(attrs.href, attrs.target)\n\n return true\n }\n\n return false\n },\n },\n })\n}\n","import { Editor } from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\nimport { find } from 'linkifyjs'\n\ntype PasteHandlerOptions = {\n editor: Editor,\n type: MarkType,\n}\n\nexport default function pasteHandler(options: PasteHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, event, slice) => {\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent).find(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n\n return true\n },\n },\n })\n}\n","import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'\nimport { find } from 'linkifyjs'\nimport autolink from './helpers/autolink'\nimport clickHandler from './helpers/clickHandler'\nimport pasteHandler from './helpers/pasteHandler'\n\nexport interface LinkOptions {\n /**\n * If enabled, it adds links as you type.\n */\n autolink: boolean,\n /**\n * If enabled, links will be opened on click.\n */\n openOnClick: boolean,\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n */\n linkOnPaste: boolean,\n /**\n * A list of HTML attributes to be rendered.\n */\n HTMLAttributes: Record<string, any>,\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n */\n setLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Toggle a link mark\n */\n toggleLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Unset a link mark\n */\n unsetLink: () => ReturnType,\n }\n }\n}\n\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n keepOnSplit: false,\n\n inclusive() {\n return this.options.autolink\n },\n\n addOptions() {\n return {\n openOnClick: true,\n linkOnPaste: true,\n autolink: true,\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n },\n target: {\n default: this.options.HTMLAttributes.target,\n },\n }\n },\n\n parseHTML() {\n return [\n { tag: 'a[href]' },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return [\n 'a',\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n 0,\n ]\n },\n\n addCommands() {\n return {\n setLink: attributes => ({ commands }) => {\n return commands.setMark(this.name, attributes)\n },\n\n toggleLink: attributes => ({ commands }) => {\n return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n },\n\n unsetLink: () => ({ commands }) => {\n return commands.unsetMark(this.name, { extendEmptyMarkRange: true })\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => find(text)\n .filter(link => link.isLink)\n .map(link => ({\n text: link.value,\n index: link.start,\n data: link,\n })),\n type: this.type,\n getAttributes: match => ({\n href: match.data?.href,\n }),\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins = []\n\n if (this.options.autolink) {\n plugins.push(autolink({\n type: this.type,\n }))\n }\n\n if (this.options.openOnClick) {\n plugins.push(clickHandler({\n type: this.type,\n }))\n }\n\n if (this.options.linkOnPaste) {\n plugins.push(pasteHandler({\n editor: this.editor,\n type: this.type,\n }))\n }\n\n return plugins\n },\n})\n"],"names":[],"mappings":";;;;SAcwB,QAAQ,CAAC,OAAwB;IACvD,OAAO,IAAI,MAAM,CAAC;QAChB,GAAG,EAAE,IAAI,SAAS,CAAC,UAAU,CAAC;QAC9B,iBAAiB,EAAE,CAAC,YAAY,EAAE,QAAQ,EAAE,QAAQ;YAClD,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,UAAU,CAAC;mBACtE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;YAEnC,IAAI,CAAC,UAAU,EAAE;gBACf,OAAM;aACP;YAED,MAAM,EAAE,EAAE,EAAE,GAAG,QAAQ,CAAA;YACvB,MAAM,SAAS,GAAG,uBAAuB,CAAC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;YACrE,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,CAAA;YAC7B,MAAM,OAAO,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAA;YAE3C,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE;;gBAErC,eAAe,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,GAAG,CAAC;qBACtD,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC;qBAC/C,OAAO,CAAC,OAAO;oBACd,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;oBACzC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;oBACrC,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC;yBAC3D,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;oBAElD,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;wBACpB,OAAM;qBACP;oBAED,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;oBAC3B,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,CAAA;oBACtF,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,CAAA;oBACtF,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,CAAA;oBACjC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,CAAA;;;oBAIhC,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE;wBACtB,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;qBACtD;iBACF,CAAC,CAAA;;gBAGJ,mBAAmB,CAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC;qBAClE,OAAO,CAAC,SAAS;;;oBAGhB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CACnC,SAAS,CAAC,GAAG,EACb,SAAS,CAAC,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,QAAQ,EACvC,SAAS,EACT,GAAG,CACJ,CAAA;oBAED,IAAI,CAAC,IAAI,CAAC;yBACP,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC;;yBAE3B,GAAG,CAAC,IAAI,KAAK;wBACZ,GAAG,IAAI;wBACP,IAAI,EAAE,SAAS,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC;wBACpC,EAAE,EAAE,SAAS,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC;qBACjC,CAAC,CAAC;;yBAEF,MAAM,CAAC,IAAI;wBACV,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAA;wBAC5E,MAAM,WAAW,GAAG,QAAQ,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,CAAA;wBAEtE,OAAO,aAAa,IAAI,WAAW,CAAA;qBACpC,CAAC;;yBAED,OAAO,CAAC,IAAI;wBACX,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;4BACjD,IAAI,EAAE,IAAI,CAAC,IAAI;yBAChB,CAAC,CAAC,CAAA;qBACJ,CAAC,CAAA;iBACL,CAAC,CAAA;aACL,CAAC,CAAA;YAEF,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE;gBACpB,OAAM;aACP;YAED,OAAO,EAAE,CAAA;SACV;KACF,CAAC,CAAA;AACJ;;SC5FwB,YAAY,CAAC,OAA4B;IAC/D,OAAO,IAAI,MAAM,CAAC;QAChB,GAAG,EAAE,IAAI,SAAS,CAAC,iBAAiB,CAAC;QACrC,KAAK,EAAE;YACL,WAAW,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK;;gBAC5B,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAC1D,MAAM,IAAI,GAAG,MAAC,KAAK,CAAC,MAAsB,0CAAE,OAAO,CAAC,GAAG,CAAC,CAAA;gBAExD,IAAI,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE;oBACtB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;oBAErC,OAAO,IAAI,CAAA;iBACZ;gBAED,OAAO,KAAK,CAAA;aACb;SACF;KACF,CAAC,CAAA;AACJ;;SChBwB,YAAY,CAAC,OAA4B;IAC/D,OAAO,IAAI,MAAM,CAAC;QAChB,GAAG,EAAE,IAAI,SAAS,CAAC,iBAAiB,CAAC;QACrC,KAAK,EAAE;YACL,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK;gBAC9B,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;gBACtB,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;gBAC3B,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAA;gBAE3B,IAAI,KAAK,EAAE;oBACT,OAAO,KAAK,CAAA;iBACb;gBAED,IAAI,WAAW,GAAG,EAAE,CAAA;gBAEpB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI;oBACxB,WAAW,IAAI,IAAI,CAAC,WAAW,CAAA;iBAChC,CAAC,CAAA;gBAEF,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAA;gBAEtF,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,EAAE;oBACzB,OAAO,KAAK,CAAA;iBACb;gBAED,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE;oBAC5C,IAAI,EAAE,IAAI,CAAC,IAAI;iBAChB,CAAC,CAAA;gBAEF,OAAO,IAAI,CAAA;aACZ;SACF;KACF,CAAC,CAAA;AACJ;;MCCa,IAAI,GAAG,IAAI,CAAC,MAAM,CAAc;IAC3C,IAAI,EAAE,MAAM;IAEZ,QAAQ,EAAE,IAAI;IAEd,WAAW,EAAE,KAAK;IAElB,SAAS;QACP,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAA;KAC7B;IAED,UAAU;QACR,OAAO;YACL,WAAW,EAAE,IAAI;YACjB,WAAW,EAAE,IAAI;YACjB,QAAQ,EAAE,IAAI;YACd,cAAc,EAAE;gBACd,MAAM,EAAE,QAAQ;gBAChB,GAAG,EAAE,8BAA8B;aACpC;SACF,CAAA;KACF;IAED,aAAa;QACX,OAAO;YACL,IAAI,EAAE;gBACJ,OAAO,EAAE,IAAI;aACd;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM;aAC5C;SACF,CAAA;KACF;IAED,SAAS;QACP,OAAO;YACL,EAAE,GAAG,EAAE,SAAS,EAAE;SACnB,CAAA;KACF;IAED,UAAU,CAAC,EAAE,cAAc,EAAE;QAC3B,OAAO;YACL,GAAG;YACH,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,cAAc,CAAC;YAC5D,CAAC;SACF,CAAA;KACF;IAED,WAAW;QACT,OAAO;YACL,OAAO,EAAE,UAAU,IAAI,CAAC,EAAE,QAAQ,EAAE;gBAClC,OAAO,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;aAC/C;YAED,UAAU,EAAE,UAAU,IAAI,CAAC,EAAE,QAAQ,EAAE;gBACrC,OAAO,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,CAAA;aAClF;YAED,SAAS,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE;gBAC5B,OAAO,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,CAAA;aACrE;SACF,CAAA;KACF;IAED,aAAa;QACX,OAAO;YACL,aAAa,CAAC;gBACZ,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;qBACrB,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC;qBAC3B,GAAG,CAAC,IAAI,KAAK;oBACZ,IAAI,EAAE,IAAI,CAAC,KAAK;oBAChB,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,IAAI,EAAE,IAAI;iBACX,CAAC,CAAC;gBACL,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,aAAa,EAAE,KAAK;;oBAAI,QAAC;wBACvB,IAAI,EAAE,MAAA,KAAK,CAAC,IAAI,0CAAE,IAAI;qBACvB,EAAC;iBAAA;aACH,CAAC;SACH,CAAA;KACF;IAED,qBAAqB;QACnB,MAAM,OAAO,GAAG,EAAE,CAAA;QAElB,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE;YACzB,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACpB,IAAI,EAAE,IAAI,CAAC,IAAI;aAChB,CAAC,CAAC,CAAA;SACJ;QAED,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;YAC5B,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC;gBACxB,IAAI,EAAE,IAAI,CAAC,IAAI;aAChB,CAAC,CAAC,CAAA;SACJ;QAED,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;YAC5B,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC;gBACxB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;aAChB,CAAC,CAAC,CAAA;SACJ;QAED,OAAO,OAAO,CAAA;KACf;CACF;;;;"}
@@ -1,17 +1,139 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/core'), require('prosemirror-state'), require('linkifyjs')) :
3
- typeof define === 'function' && define.amd ? define(['exports', '@tiptap/core', 'prosemirror-state', 'linkifyjs'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/extension-link"] = {}, global.core, global.prosemirrorState, global.linkifyjs));
5
- })(this, (function (exports, core, prosemirrorState, linkifyjs) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@tiptap/core'), require('linkifyjs'), require('prosemirror-state')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', '@tiptap/core', 'linkifyjs', 'prosemirror-state'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@tiptap/extension-link"] = {}, global.core, global.linkifyjs, global.prosemirrorState));
5
+ })(this, (function (exports, core, linkifyjs, prosemirrorState) { 'use strict';
6
+
7
+ function autolink(options) {
8
+ return new prosemirrorState.Plugin({
9
+ key: new prosemirrorState.PluginKey('autolink'),
10
+ appendTransaction: (transactions, oldState, newState) => {
11
+ const docChanges = transactions.some(transaction => transaction.docChanged)
12
+ && !oldState.doc.eq(newState.doc);
13
+ if (!docChanges) {
14
+ return;
15
+ }
16
+ const { tr } = newState;
17
+ const transform = core.combineTransactionSteps(oldState.doc, transactions);
18
+ const { mapping } = transform;
19
+ const changes = core.getChangedRanges(transform);
20
+ changes.forEach(({ oldRange, newRange }) => {
21
+ // at first we check if we have to remove links
22
+ core.getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
23
+ .filter(item => item.mark.type === options.type)
24
+ .forEach(oldMark => {
25
+ const newFrom = mapping.map(oldMark.from);
26
+ const newTo = mapping.map(oldMark.to);
27
+ const newMarks = core.getMarksBetween(newFrom, newTo, newState.doc)
28
+ .filter(item => item.mark.type === options.type);
29
+ if (!newMarks.length) {
30
+ return;
31
+ }
32
+ const newMark = newMarks[0];
33
+ const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ');
34
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ');
35
+ const wasLink = linkifyjs.test(oldLinkText);
36
+ const isLink = linkifyjs.test(newLinkText);
37
+ // remove only the link, if it was a link before too
38
+ // because we don’t want to remove links that were set manually
39
+ if (wasLink && !isLink) {
40
+ tr.removeMark(newMark.from, newMark.to, options.type);
41
+ }
42
+ });
43
+ // now let’s see if we can add new links
44
+ core.findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
45
+ .forEach(textBlock => {
46
+ // we need to define a placeholder for leaf nodes
47
+ // so that the link position can be calculated correctly
48
+ const text = newState.doc.textBetween(textBlock.pos, textBlock.pos + textBlock.node.nodeSize, undefined, ' ');
49
+ linkifyjs.find(text)
50
+ .filter(link => link.isLink)
51
+ // calculate link position
52
+ .map(link => ({
53
+ ...link,
54
+ from: textBlock.pos + link.start + 1,
55
+ to: textBlock.pos + link.end + 1,
56
+ }))
57
+ // check if link is within the changed range
58
+ .filter(link => {
59
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
60
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
61
+ return fromIsInRange || toIsInRange;
62
+ })
63
+ // add link mark
64
+ .forEach(link => {
65
+ tr.addMark(link.from, link.to, options.type.create({
66
+ href: link.href,
67
+ }));
68
+ });
69
+ });
70
+ });
71
+ if (!tr.steps.length) {
72
+ return;
73
+ }
74
+ return tr;
75
+ },
76
+ });
77
+ }
78
+
79
+ function clickHandler(options) {
80
+ return new prosemirrorState.Plugin({
81
+ key: new prosemirrorState.PluginKey('handleClickLink'),
82
+ props: {
83
+ handleClick: (view, pos, event) => {
84
+ var _a;
85
+ const attrs = core.getAttributes(view.state, options.type.name);
86
+ const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
87
+ if (link && attrs.href) {
88
+ window.open(attrs.href, attrs.target);
89
+ return true;
90
+ }
91
+ return false;
92
+ },
93
+ },
94
+ });
95
+ }
96
+
97
+ function pasteHandler(options) {
98
+ return new prosemirrorState.Plugin({
99
+ key: new prosemirrorState.PluginKey('handlePasteLink'),
100
+ props: {
101
+ handlePaste: (view, event, slice) => {
102
+ const { state } = view;
103
+ const { selection } = state;
104
+ const { empty } = selection;
105
+ if (empty) {
106
+ return false;
107
+ }
108
+ let textContent = '';
109
+ slice.content.forEach(node => {
110
+ textContent += node.textContent;
111
+ });
112
+ const link = linkifyjs.find(textContent).find(item => item.isLink && item.value === textContent);
113
+ if (!textContent || !link) {
114
+ return false;
115
+ }
116
+ options.editor.commands.setMark(options.type, {
117
+ href: link.href,
118
+ });
119
+ return true;
120
+ },
121
+ },
122
+ });
123
+ }
6
124
 
7
125
  const Link = core.Mark.create({
8
126
  name: 'link',
9
127
  priority: 1000,
10
- inclusive: false,
128
+ keepOnSplit: false,
129
+ inclusive() {
130
+ return this.options.autolink;
131
+ },
11
132
  addOptions() {
12
133
  return {
13
134
  openOnClick: true,
14
135
  linkOnPaste: true,
136
+ autolink: true,
15
137
  HTMLAttributes: {
16
138
  target: '_blank',
17
139
  rel: 'noopener noreferrer nofollow',
@@ -34,7 +156,11 @@
34
156
  ];
35
157
  },
36
158
  renderHTML({ HTMLAttributes }) {
37
- return ['a', core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
159
+ return [
160
+ 'a',
161
+ core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
162
+ 0,
163
+ ];
38
164
  },
39
165
  addCommands() {
40
166
  return {
@@ -71,49 +197,20 @@
71
197
  },
72
198
  addProseMirrorPlugins() {
73
199
  const plugins = [];
200
+ if (this.options.autolink) {
201
+ plugins.push(autolink({
202
+ type: this.type,
203
+ }));
204
+ }
74
205
  if (this.options.openOnClick) {
75
- plugins.push(new prosemirrorState.Plugin({
76
- key: new prosemirrorState.PluginKey('handleClickLink'),
77
- props: {
78
- handleClick: (view, pos, event) => {
79
- var _a;
80
- const attrs = this.editor.getAttributes(this.name);
81
- const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
82
- if (link && attrs.href) {
83
- window.open(attrs.href, attrs.target);
84
- return true;
85
- }
86
- return false;
87
- },
88
- },
206
+ plugins.push(clickHandler({
207
+ type: this.type,
89
208
  }));
90
209
  }
91
210
  if (this.options.linkOnPaste) {
92
- plugins.push(new prosemirrorState.Plugin({
93
- key: new prosemirrorState.PluginKey('handlePasteLink'),
94
- props: {
95
- handlePaste: (view, event, slice) => {
96
- const { state } = view;
97
- const { selection } = state;
98
- const { empty } = selection;
99
- if (empty) {
100
- return false;
101
- }
102
- let textContent = '';
103
- slice.content.forEach(node => {
104
- textContent += node.textContent;
105
- });
106
- const link = linkifyjs.find(textContent)
107
- .find(item => item.isLink && item.value === textContent);
108
- if (!textContent || !link) {
109
- return false;
110
- }
111
- this.editor.commands.setMark(this.type, {
112
- href: link.href,
113
- });
114
- return true;
115
- },
116
- },
211
+ plugins.push(pasteHandler({
212
+ editor: this.editor,
213
+ type: this.type,
117
214
  }));
118
215
  }
119
216
  return plugins;
@@ -1 +1 @@
1
- {"version":3,"file":"tiptap-extension-link.umd.js","sources":["../src/link.ts"],"sourcesContent":["import {\n Mark,\n markPasteRule,\n mergeAttributes,\n} from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { find } from 'linkifyjs'\n\nexport interface LinkOptions {\n /**\n * If enabled, links will be opened on click.\n */\n openOnClick: boolean,\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n */\n linkOnPaste: boolean,\n /**\n * A list of HTML attributes to be rendered.\n */\n HTMLAttributes: Record<string, any>,\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n */\n setLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Toggle a link mark\n */\n toggleLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Unset a link mark\n */\n unsetLink: () => ReturnType,\n }\n }\n}\n\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n inclusive: false,\n\n addOptions() {\n return {\n openOnClick: true,\n linkOnPaste: true,\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n },\n target: {\n default: this.options.HTMLAttributes.target,\n },\n }\n },\n\n parseHTML() {\n return [\n { tag: 'a[href]' },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]\n },\n\n addCommands() {\n return {\n setLink: attributes => ({ commands }) => {\n return commands.setMark(this.name, attributes)\n },\n toggleLink: attributes => ({ commands }) => {\n return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n },\n unsetLink: () => ({ commands }) => {\n return commands.unsetMark(this.name, { extendEmptyMarkRange: true })\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => find(text)\n .filter(link => link.isLink)\n .map(link => ({\n text: link.value,\n index: link.start,\n data: link,\n })),\n type: this.type,\n getAttributes: match => ({\n href: match.data?.href,\n }),\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins = []\n\n if (this.options.openOnClick) {\n plugins.push(\n new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n const attrs = this.editor.getAttributes(this.name)\n const link = (event.target as HTMLElement)?.closest('a')\n\n if (link && attrs.href) {\n window.open(attrs.href, attrs.target)\n\n return true\n }\n\n return false\n },\n },\n }),\n )\n }\n\n if (this.options.linkOnPaste) {\n plugins.push(\n new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, event, slice) => {\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent)\n .find(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n this.editor.commands.setMark(this.type, {\n href: link.href,\n })\n\n return true\n },\n },\n }),\n )\n }\n\n return plugins\n },\n})\n"],"names":["Mark","mergeAttributes","markPasteRule","find","Plugin","PluginKey"],"mappings":"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"}
1
+ {"version":3,"file":"tiptap-extension-link.umd.js","sources":["../src/helpers/autolink.ts","../src/helpers/clickHandler.ts","../src/helpers/pasteHandler.ts","../src/link.ts"],"sourcesContent":["import {\n getMarksBetween,\n findChildrenInRange,\n combineTransactionSteps,\n getChangedRanges,\n} from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\nimport { find, test } from 'linkifyjs'\n\ntype AutolinkOptions = {\n type: MarkType,\n}\n\nexport default function autolink(options: AutolinkOptions): Plugin {\n return new Plugin({\n key: new PluginKey('autolink'),\n appendTransaction: (transactions, oldState, newState) => {\n const docChanges = transactions.some(transaction => transaction.docChanged)\n && !oldState.doc.eq(newState.doc)\n\n if (!docChanges) {\n return\n }\n\n const { tr } = newState\n const transform = combineTransactionSteps(oldState.doc, transactions)\n const { mapping } = transform\n const changes = getChangedRanges(transform)\n\n changes.forEach(({ oldRange, newRange }) => {\n // at first we check if we have to remove links\n getMarksBetween(oldRange.from, oldRange.to, oldState.doc)\n .filter(item => item.mark.type === options.type)\n .forEach(oldMark => {\n const newFrom = mapping.map(oldMark.from)\n const newTo = mapping.map(oldMark.to)\n const newMarks = getMarksBetween(newFrom, newTo, newState.doc)\n .filter(item => item.mark.type === options.type)\n\n if (!newMarks.length) {\n return\n }\n\n const newMark = newMarks[0]\n const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ')\n const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ')\n const wasLink = test(oldLinkText)\n const isLink = test(newLinkText)\n\n // remove only the link, if it was a link before too\n // because we don’t want to remove links that were set manually\n if (wasLink && !isLink) {\n tr.removeMark(newMark.from, newMark.to, options.type)\n }\n })\n\n // now let’s see if we can add new links\n findChildrenInRange(newState.doc, newRange, node => node.isTextblock)\n .forEach(textBlock => {\n // we need to define a placeholder for leaf nodes\n // so that the link position can be calculated correctly\n const text = newState.doc.textBetween(\n textBlock.pos,\n textBlock.pos + textBlock.node.nodeSize,\n undefined,\n ' ',\n )\n\n find(text)\n .filter(link => link.isLink)\n // calculate link position\n .map(link => ({\n ...link,\n from: textBlock.pos + link.start + 1,\n to: textBlock.pos + link.end + 1,\n }))\n // check if link is within the changed range\n .filter(link => {\n const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to\n const toIsInRange = newRange.to >= link.from && newRange.to <= link.to\n\n return fromIsInRange || toIsInRange\n })\n // add link mark\n .forEach(link => {\n tr.addMark(link.from, link.to, options.type.create({\n href: link.href,\n }))\n })\n })\n })\n\n if (!tr.steps.length) {\n return\n }\n\n return tr\n },\n })\n}\n","import { getAttributes } from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\n\ntype ClickHandlerOptions = {\n type: MarkType,\n}\n\nexport default function clickHandler(options: ClickHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handleClickLink'),\n props: {\n handleClick: (view, pos, event) => {\n const attrs = getAttributes(view.state, options.type.name)\n const link = (event.target as HTMLElement)?.closest('a')\n\n if (link && attrs.href) {\n window.open(attrs.href, attrs.target)\n\n return true\n }\n\n return false\n },\n },\n })\n}\n","import { Editor } from '@tiptap/core'\nimport { Plugin, PluginKey } from 'prosemirror-state'\nimport { MarkType } from 'prosemirror-model'\nimport { find } from 'linkifyjs'\n\ntype PasteHandlerOptions = {\n editor: Editor,\n type: MarkType,\n}\n\nexport default function pasteHandler(options: PasteHandlerOptions): Plugin {\n return new Plugin({\n key: new PluginKey('handlePasteLink'),\n props: {\n handlePaste: (view, event, slice) => {\n const { state } = view\n const { selection } = state\n const { empty } = selection\n\n if (empty) {\n return false\n }\n\n let textContent = ''\n\n slice.content.forEach(node => {\n textContent += node.textContent\n })\n\n const link = find(textContent).find(item => item.isLink && item.value === textContent)\n\n if (!textContent || !link) {\n return false\n }\n\n options.editor.commands.setMark(options.type, {\n href: link.href,\n })\n\n return true\n },\n },\n })\n}\n","import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'\nimport { find } from 'linkifyjs'\nimport autolink from './helpers/autolink'\nimport clickHandler from './helpers/clickHandler'\nimport pasteHandler from './helpers/pasteHandler'\n\nexport interface LinkOptions {\n /**\n * If enabled, it adds links as you type.\n */\n autolink: boolean,\n /**\n * If enabled, links will be opened on click.\n */\n openOnClick: boolean,\n /**\n * Adds a link to the current selection if the pasted content only contains an url.\n */\n linkOnPaste: boolean,\n /**\n * A list of HTML attributes to be rendered.\n */\n HTMLAttributes: Record<string, any>,\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n link: {\n /**\n * Set a link mark\n */\n setLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Toggle a link mark\n */\n toggleLink: (attributes: { href: string, target?: string }) => ReturnType,\n /**\n * Unset a link mark\n */\n unsetLink: () => ReturnType,\n }\n }\n}\n\nexport const Link = Mark.create<LinkOptions>({\n name: 'link',\n\n priority: 1000,\n\n keepOnSplit: false,\n\n inclusive() {\n return this.options.autolink\n },\n\n addOptions() {\n return {\n openOnClick: true,\n linkOnPaste: true,\n autolink: true,\n HTMLAttributes: {\n target: '_blank',\n rel: 'noopener noreferrer nofollow',\n },\n }\n },\n\n addAttributes() {\n return {\n href: {\n default: null,\n },\n target: {\n default: this.options.HTMLAttributes.target,\n },\n }\n },\n\n parseHTML() {\n return [\n { tag: 'a[href]' },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return [\n 'a',\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),\n 0,\n ]\n },\n\n addCommands() {\n return {\n setLink: attributes => ({ commands }) => {\n return commands.setMark(this.name, attributes)\n },\n\n toggleLink: attributes => ({ commands }) => {\n return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })\n },\n\n unsetLink: () => ({ commands }) => {\n return commands.unsetMark(this.name, { extendEmptyMarkRange: true })\n },\n }\n },\n\n addPasteRules() {\n return [\n markPasteRule({\n find: text => find(text)\n .filter(link => link.isLink)\n .map(link => ({\n text: link.value,\n index: link.start,\n data: link,\n })),\n type: this.type,\n getAttributes: match => ({\n href: match.data?.href,\n }),\n }),\n ]\n },\n\n addProseMirrorPlugins() {\n const plugins = []\n\n if (this.options.autolink) {\n plugins.push(autolink({\n type: this.type,\n }))\n }\n\n if (this.options.openOnClick) {\n plugins.push(clickHandler({\n type: this.type,\n }))\n }\n\n if (this.options.linkOnPaste) {\n plugins.push(pasteHandler({\n editor: this.editor,\n type: this.type,\n }))\n }\n\n return plugins\n },\n})\n"],"names":["Plugin","PluginKey","combineTransactionSteps","getChangedRanges","getMarksBetween","test","findChildrenInRange","find","getAttributes","Mark","mergeAttributes","markPasteRule"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/extension-link",
3
3
  "description": "link extension for tiptap",
4
- "version": "2.0.0-beta.29",
4
+ "version": "2.0.0-beta.32",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -24,7 +24,8 @@
24
24
  "@tiptap/core": "^2.0.0-beta.1"
25
25
  },
26
26
  "dependencies": {
27
- "linkifyjs": "^3.0.3",
27
+ "linkifyjs": "^3.0.4",
28
+ "prosemirror-model": "^1.15.0",
28
29
  "prosemirror-state": "^1.3.4"
29
30
  },
30
31
  "repository": {
@@ -32,5 +33,5 @@
32
33
  "url": "https://github.com/ueberdosis/tiptap",
33
34
  "directory": "packages/extension-link"
34
35
  },
35
- "gitHead": "270543995c92243fc27c4a688af6a93033fb4b7e"
36
+ "gitHead": "50e15cb616332ced76f000b9cc921521cda20a8d"
36
37
  }
@@ -0,0 +1,101 @@
1
+ import {
2
+ getMarksBetween,
3
+ findChildrenInRange,
4
+ combineTransactionSteps,
5
+ getChangedRanges,
6
+ } from '@tiptap/core'
7
+ import { Plugin, PluginKey } from 'prosemirror-state'
8
+ import { MarkType } from 'prosemirror-model'
9
+ import { find, test } from 'linkifyjs'
10
+
11
+ type AutolinkOptions = {
12
+ type: MarkType,
13
+ }
14
+
15
+ export default function autolink(options: AutolinkOptions): Plugin {
16
+ return new Plugin({
17
+ key: new PluginKey('autolink'),
18
+ appendTransaction: (transactions, oldState, newState) => {
19
+ const docChanges = transactions.some(transaction => transaction.docChanged)
20
+ && !oldState.doc.eq(newState.doc)
21
+
22
+ if (!docChanges) {
23
+ return
24
+ }
25
+
26
+ const { tr } = newState
27
+ const transform = combineTransactionSteps(oldState.doc, transactions)
28
+ const { mapping } = transform
29
+ const changes = getChangedRanges(transform)
30
+
31
+ changes.forEach(({ oldRange, newRange }) => {
32
+ // at first we check if we have to remove links
33
+ getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
34
+ .filter(item => item.mark.type === options.type)
35
+ .forEach(oldMark => {
36
+ const newFrom = mapping.map(oldMark.from)
37
+ const newTo = mapping.map(oldMark.to)
38
+ const newMarks = getMarksBetween(newFrom, newTo, newState.doc)
39
+ .filter(item => item.mark.type === options.type)
40
+
41
+ if (!newMarks.length) {
42
+ return
43
+ }
44
+
45
+ const newMark = newMarks[0]
46
+ const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ')
47
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ')
48
+ const wasLink = test(oldLinkText)
49
+ const isLink = test(newLinkText)
50
+
51
+ // remove only the link, if it was a link before too
52
+ // because we don’t want to remove links that were set manually
53
+ if (wasLink && !isLink) {
54
+ tr.removeMark(newMark.from, newMark.to, options.type)
55
+ }
56
+ })
57
+
58
+ // now let’s see if we can add new links
59
+ findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
60
+ .forEach(textBlock => {
61
+ // we need to define a placeholder for leaf nodes
62
+ // so that the link position can be calculated correctly
63
+ const text = newState.doc.textBetween(
64
+ textBlock.pos,
65
+ textBlock.pos + textBlock.node.nodeSize,
66
+ undefined,
67
+ ' ',
68
+ )
69
+
70
+ find(text)
71
+ .filter(link => link.isLink)
72
+ // calculate link position
73
+ .map(link => ({
74
+ ...link,
75
+ from: textBlock.pos + link.start + 1,
76
+ to: textBlock.pos + link.end + 1,
77
+ }))
78
+ // check if link is within the changed range
79
+ .filter(link => {
80
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to
81
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to
82
+
83
+ return fromIsInRange || toIsInRange
84
+ })
85
+ // add link mark
86
+ .forEach(link => {
87
+ tr.addMark(link.from, link.to, options.type.create({
88
+ href: link.href,
89
+ }))
90
+ })
91
+ })
92
+ })
93
+
94
+ if (!tr.steps.length) {
95
+ return
96
+ }
97
+
98
+ return tr
99
+ },
100
+ })
101
+ }
@@ -0,0 +1,27 @@
1
+ import { getAttributes } from '@tiptap/core'
2
+ import { Plugin, PluginKey } from 'prosemirror-state'
3
+ import { MarkType } from 'prosemirror-model'
4
+
5
+ type ClickHandlerOptions = {
6
+ type: MarkType,
7
+ }
8
+
9
+ export default function clickHandler(options: ClickHandlerOptions): Plugin {
10
+ return new Plugin({
11
+ key: new PluginKey('handleClickLink'),
12
+ props: {
13
+ handleClick: (view, pos, event) => {
14
+ const attrs = getAttributes(view.state, options.type.name)
15
+ const link = (event.target as HTMLElement)?.closest('a')
16
+
17
+ if (link && attrs.href) {
18
+ window.open(attrs.href, attrs.target)
19
+
20
+ return true
21
+ }
22
+
23
+ return false
24
+ },
25
+ },
26
+ })
27
+ }
@@ -0,0 +1,44 @@
1
+ import { Editor } from '@tiptap/core'
2
+ import { Plugin, PluginKey } from 'prosemirror-state'
3
+ import { MarkType } from 'prosemirror-model'
4
+ import { find } from 'linkifyjs'
5
+
6
+ type PasteHandlerOptions = {
7
+ editor: Editor,
8
+ type: MarkType,
9
+ }
10
+
11
+ export default function pasteHandler(options: PasteHandlerOptions): Plugin {
12
+ return new Plugin({
13
+ key: new PluginKey('handlePasteLink'),
14
+ props: {
15
+ handlePaste: (view, event, slice) => {
16
+ const { state } = view
17
+ const { selection } = state
18
+ const { empty } = selection
19
+
20
+ if (empty) {
21
+ return false
22
+ }
23
+
24
+ let textContent = ''
25
+
26
+ slice.content.forEach(node => {
27
+ textContent += node.textContent
28
+ })
29
+
30
+ const link = find(textContent).find(item => item.isLink && item.value === textContent)
31
+
32
+ if (!textContent || !link) {
33
+ return false
34
+ }
35
+
36
+ options.editor.commands.setMark(options.type, {
37
+ href: link.href,
38
+ })
39
+
40
+ return true
41
+ },
42
+ },
43
+ })
44
+ }
package/src/link.ts CHANGED
@@ -1,12 +1,14 @@
1
- import {
2
- Mark,
3
- markPasteRule,
4
- mergeAttributes,
5
- } from '@tiptap/core'
6
- import { Plugin, PluginKey } from 'prosemirror-state'
1
+ import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'
7
2
  import { find } from 'linkifyjs'
3
+ import autolink from './helpers/autolink'
4
+ import clickHandler from './helpers/clickHandler'
5
+ import pasteHandler from './helpers/pasteHandler'
8
6
 
9
7
  export interface LinkOptions {
8
+ /**
9
+ * If enabled, it adds links as you type.
10
+ */
11
+ autolink: boolean,
10
12
  /**
11
13
  * If enabled, links will be opened on click.
12
14
  */
@@ -45,12 +47,17 @@ export const Link = Mark.create<LinkOptions>({
45
47
 
46
48
  priority: 1000,
47
49
 
48
- inclusive: false,
50
+ keepOnSplit: false,
51
+
52
+ inclusive() {
53
+ return this.options.autolink
54
+ },
49
55
 
50
56
  addOptions() {
51
57
  return {
52
58
  openOnClick: true,
53
59
  linkOnPaste: true,
60
+ autolink: true,
54
61
  HTMLAttributes: {
55
62
  target: '_blank',
56
63
  rel: 'noopener noreferrer nofollow',
@@ -76,7 +83,11 @@ export const Link = Mark.create<LinkOptions>({
76
83
  },
77
84
 
78
85
  renderHTML({ HTMLAttributes }) {
79
- return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
86
+ return [
87
+ 'a',
88
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
89
+ 0,
90
+ ]
80
91
  },
81
92
 
82
93
  addCommands() {
@@ -84,9 +95,11 @@ export const Link = Mark.create<LinkOptions>({
84
95
  setLink: attributes => ({ commands }) => {
85
96
  return commands.setMark(this.name, attributes)
86
97
  },
98
+
87
99
  toggleLink: attributes => ({ commands }) => {
88
100
  return commands.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
89
101
  },
102
+
90
103
  unsetLink: () => ({ commands }) => {
91
104
  return commands.unsetMark(this.name, { extendEmptyMarkRange: true })
92
105
  },
@@ -114,64 +127,23 @@ export const Link = Mark.create<LinkOptions>({
114
127
  addProseMirrorPlugins() {
115
128
  const plugins = []
116
129
 
130
+ if (this.options.autolink) {
131
+ plugins.push(autolink({
132
+ type: this.type,
133
+ }))
134
+ }
135
+
117
136
  if (this.options.openOnClick) {
118
- plugins.push(
119
- new Plugin({
120
- key: new PluginKey('handleClickLink'),
121
- props: {
122
- handleClick: (view, pos, event) => {
123
- const attrs = this.editor.getAttributes(this.name)
124
- const link = (event.target as HTMLElement)?.closest('a')
125
-
126
- if (link && attrs.href) {
127
- window.open(attrs.href, attrs.target)
128
-
129
- return true
130
- }
131
-
132
- return false
133
- },
134
- },
135
- }),
136
- )
137
+ plugins.push(clickHandler({
138
+ type: this.type,
139
+ }))
137
140
  }
138
141
 
139
142
  if (this.options.linkOnPaste) {
140
- plugins.push(
141
- new Plugin({
142
- key: new PluginKey('handlePasteLink'),
143
- props: {
144
- handlePaste: (view, event, slice) => {
145
- const { state } = view
146
- const { selection } = state
147
- const { empty } = selection
148
-
149
- if (empty) {
150
- return false
151
- }
152
-
153
- let textContent = ''
154
-
155
- slice.content.forEach(node => {
156
- textContent += node.textContent
157
- })
158
-
159
- const link = find(textContent)
160
- .find(item => item.isLink && item.value === textContent)
161
-
162
- if (!textContent || !link) {
163
- return false
164
- }
165
-
166
- this.editor.commands.setMark(this.type, {
167
- href: link.href,
168
- })
169
-
170
- return true
171
- },
172
- },
173
- }),
174
- )
143
+ plugins.push(pasteHandler({
144
+ editor: this.editor,
145
+ type: this.type,
146
+ }))
175
147
  }
176
148
 
177
149
  return plugins