@tiptap/extension-link 2.0.0-beta.30 → 2.0.0-beta.31

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,136 @@
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);
36
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to);
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
+ linkifyjs.find(textBlock.node.textContent)
49
+ .filter(link => link.isLink)
50
+ // calculate link position
51
+ .map(link => ({
52
+ ...link,
53
+ from: textBlock.pos + link.start + 1,
54
+ to: textBlock.pos + link.end + 1,
55
+ }))
56
+ // check if link is within the changed range
57
+ .filter(link => {
58
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
59
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
60
+ return fromIsInRange || toIsInRange;
61
+ })
62
+ // add link mark
63
+ .forEach(link => {
64
+ tr.addMark(link.from, link.to, options.type.create({
65
+ href: link.href,
66
+ }));
67
+ });
68
+ });
69
+ });
70
+ if (!tr.steps.length) {
71
+ return;
72
+ }
73
+ return tr;
74
+ },
75
+ });
76
+ }
77
+
78
+ function clickHandler(options) {
79
+ return new prosemirrorState.Plugin({
80
+ key: new prosemirrorState.PluginKey('handleClickLink'),
81
+ props: {
82
+ handleClick: (view, pos, event) => {
83
+ var _a;
84
+ const attrs = core.getAttributes(view.state, options.type.name);
85
+ const link = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a');
86
+ if (link && attrs.href) {
87
+ window.open(attrs.href, attrs.target);
88
+ return true;
89
+ }
90
+ return false;
91
+ },
92
+ },
93
+ });
94
+ }
95
+
96
+ function pasteHandler(options) {
97
+ return new prosemirrorState.Plugin({
98
+ key: new prosemirrorState.PluginKey('handlePasteLink'),
99
+ props: {
100
+ handlePaste: (view, event, slice) => {
101
+ const { state } = view;
102
+ const { selection } = state;
103
+ const { empty } = selection;
104
+ if (empty) {
105
+ return false;
106
+ }
107
+ let textContent = '';
108
+ slice.content.forEach(node => {
109
+ textContent += node.textContent;
110
+ });
111
+ const link = linkifyjs.find(textContent).find(item => item.isLink && item.value === textContent);
112
+ if (!textContent || !link) {
113
+ return false;
114
+ }
115
+ options.editor.commands.setMark(options.type, {
116
+ href: link.href,
117
+ });
118
+ return true;
119
+ },
120
+ },
121
+ });
122
+ }
8
123
 
9
124
  const Link = core.Mark.create({
10
125
  name: 'link',
11
126
  priority: 1000,
12
- inclusive: false,
127
+ keepOnSplit: false,
128
+ inclusive() {
129
+ return this.options.autolink;
130
+ },
13
131
  addOptions() {
14
132
  return {
15
133
  openOnClick: true,
16
134
  linkOnPaste: true,
135
+ autolink: true,
17
136
  HTMLAttributes: {
18
137
  target: '_blank',
19
138
  rel: 'noopener noreferrer nofollow',
@@ -36,7 +155,11 @@ const Link = core.Mark.create({
36
155
  ];
37
156
  },
38
157
  renderHTML({ HTMLAttributes }) {
39
- return ['a', core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
158
+ return [
159
+ 'a',
160
+ core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
161
+ 0,
162
+ ];
40
163
  },
41
164
  addCommands() {
42
165
  return {
@@ -73,49 +196,20 @@ const Link = core.Mark.create({
73
196
  },
74
197
  addProseMirrorPlugins() {
75
198
  const plugins = [];
199
+ if (this.options.autolink) {
200
+ plugins.push(autolink({
201
+ type: this.type,
202
+ }));
203
+ }
76
204
  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
- },
205
+ plugins.push(clickHandler({
206
+ type: this.type,
91
207
  }));
92
208
  }
93
209
  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
- },
210
+ plugins.push(pasteHandler({
211
+ editor: this.editor,
212
+ type: this.type,
119
213
  }));
120
214
  }
121
215
  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":";;;;;;;;MA0Ca,IAAI,GAAGA,SAAI,CAAC,MAAM,CAAc;IAC3C,IAAI,EAAE,MAAM;IAEZ,QAAQ,EAAE,IAAI;IAEd,SAAS,EAAE,KAAK;IAEhB,UAAU;QACR,OAAO;YACL,WAAW,EAAE,IAAI;YACjB,WAAW,EAAE,IAAI;YACjB,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,CAAC,GAAG,EAAEC,oBAAe,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC,CAAA;KAC9E;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;YACD,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;YACD,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;YACLC,kBAAa,CAAC;gBACZ,IAAI,EAAE,IAAI,IAAIC,cAAI,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,WAAW,EAAE;YAC5B,OAAO,CAAC,IAAI,CACV,IAAIC,uBAAM,CAAC;gBACT,GAAG,EAAE,IAAIC,0BAAS,CAAC,iBAAiB,CAAC;gBACrC,KAAK,EAAE;oBACL,WAAW,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK;;wBAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;wBAClD,MAAM,IAAI,GAAG,MAAC,KAAK,CAAC,MAAsB,0CAAE,OAAO,CAAC,GAAG,CAAC,CAAA;wBAExD,IAAI,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE;4BACtB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;4BAErC,OAAO,IAAI,CAAA;yBACZ;wBAED,OAAO,KAAK,CAAA;qBACb;iBACF;aACF,CAAC,CACH,CAAA;SACF;QAED,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;YAC5B,OAAO,CAAC,IAAI,CACV,IAAID,uBAAM,CAAC;gBACT,GAAG,EAAE,IAAIC,0BAAS,CAAC,iBAAiB,CAAC;gBACrC,KAAK,EAAE;oBACL,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK;wBAC9B,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;wBACtB,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;wBAC3B,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAA;wBAE3B,IAAI,KAAK,EAAE;4BACT,OAAO,KAAK,CAAA;yBACb;wBAED,IAAI,WAAW,GAAG,EAAE,CAAA;wBAEpB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI;4BACxB,WAAW,IAAI,IAAI,CAAC,WAAW,CAAA;yBAChC,CAAC,CAAA;wBAEF,MAAM,IAAI,GAAGF,cAAI,CAAC,WAAW,CAAC;6BAC3B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAA;wBAE1D,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,EAAE;4BACzB,OAAO,KAAK,CAAA;yBACb;wBAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE;4BACtC,IAAI,EAAE,IAAI,CAAC,IAAI;yBAChB,CAAC,CAAA;wBAEF,OAAO,IAAI,CAAA;qBACZ;iBACF;aACF,CAAC,CACH,CAAA;SACF;QAED,OAAO,OAAO,CAAA;KACf;CACF;;;;;"}
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)\n const newLinkText = newState.doc.textBetween(newMark.from, newMark.to)\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 find(textBlock.node.textContent)\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,134 @@
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);
32
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to);
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
+ find(textBlock.node.textContent)
45
+ .filter(link => link.isLink)
46
+ // calculate link position
47
+ .map(link => ({
48
+ ...link,
49
+ from: textBlock.pos + link.start + 1,
50
+ to: textBlock.pos + link.end + 1,
51
+ }))
52
+ // check if link is within the changed range
53
+ .filter(link => {
54
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
55
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
56
+ return fromIsInRange || toIsInRange;
57
+ })
58
+ // add link mark
59
+ .forEach(link => {
60
+ tr.addMark(link.from, link.to, options.type.create({
61
+ href: link.href,
62
+ }));
63
+ });
64
+ });
65
+ });
66
+ if (!tr.steps.length) {
67
+ return;
68
+ }
69
+ return tr;
70
+ },
71
+ });
72
+ }
73
+
74
+ function clickHandler(options) {
75
+ return new Plugin({
76
+ key: new PluginKey('handleClickLink'),
77
+ props: {
78
+ handleClick: (view, pos, event) => {
79
+ var _a;
80
+ const attrs = getAttributes(view.state, options.type.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
+ },
89
+ });
90
+ }
91
+
92
+ function pasteHandler(options) {
93
+ return new Plugin({
94
+ key: new PluginKey('handlePasteLink'),
95
+ props: {
96
+ handlePaste: (view, event, slice) => {
97
+ const { state } = view;
98
+ const { selection } = state;
99
+ const { empty } = selection;
100
+ if (empty) {
101
+ return false;
102
+ }
103
+ let textContent = '';
104
+ slice.content.forEach(node => {
105
+ textContent += node.textContent;
106
+ });
107
+ const link = find(textContent).find(item => item.isLink && item.value === textContent);
108
+ if (!textContent || !link) {
109
+ return false;
110
+ }
111
+ options.editor.commands.setMark(options.type, {
112
+ href: link.href,
113
+ });
114
+ return true;
115
+ },
116
+ },
117
+ });
118
+ }
4
119
 
5
120
  const Link = Mark.create({
6
121
  name: 'link',
7
122
  priority: 1000,
8
- inclusive: false,
123
+ keepOnSplit: false,
124
+ inclusive() {
125
+ return this.options.autolink;
126
+ },
9
127
  addOptions() {
10
128
  return {
11
129
  openOnClick: true,
12
130
  linkOnPaste: true,
131
+ autolink: true,
13
132
  HTMLAttributes: {
14
133
  target: '_blank',
15
134
  rel: 'noopener noreferrer nofollow',
@@ -32,7 +151,11 @@ const Link = Mark.create({
32
151
  ];
33
152
  },
34
153
  renderHTML({ HTMLAttributes }) {
35
- return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
154
+ return [
155
+ 'a',
156
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
157
+ 0,
158
+ ];
36
159
  },
37
160
  addCommands() {
38
161
  return {
@@ -69,49 +192,20 @@ const Link = Mark.create({
69
192
  },
70
193
  addProseMirrorPlugins() {
71
194
  const plugins = [];
195
+ if (this.options.autolink) {
196
+ plugins.push(autolink({
197
+ type: this.type,
198
+ }));
199
+ }
72
200
  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
- },
201
+ plugins.push(clickHandler({
202
+ type: this.type,
87
203
  }));
88
204
  }
89
205
  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
- },
206
+ plugins.push(pasteHandler({
207
+ editor: this.editor,
208
+ type: this.type,
115
209
  }));
116
210
  }
117
211
  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":";;;;MA0Ca,IAAI,GAAG,IAAI,CAAC,MAAM,CAAc;IAC3C,IAAI,EAAE,MAAM;IAEZ,QAAQ,EAAE,IAAI;IAEd,SAAS,EAAE,KAAK;IAEhB,UAAU;QACR,OAAO;YACL,WAAW,EAAE,IAAI;YACjB,WAAW,EAAE,IAAI;YACjB,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,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC,CAAA;KAC9E;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;YACD,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;YACD,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,WAAW,EAAE;YAC5B,OAAO,CAAC,IAAI,CACV,IAAI,MAAM,CAAC;gBACT,GAAG,EAAE,IAAI,SAAS,CAAC,iBAAiB,CAAC;gBACrC,KAAK,EAAE;oBACL,WAAW,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK;;wBAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;wBAClD,MAAM,IAAI,GAAG,MAAC,KAAK,CAAC,MAAsB,0CAAE,OAAO,CAAC,GAAG,CAAC,CAAA;wBAExD,IAAI,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE;4BACtB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;4BAErC,OAAO,IAAI,CAAA;yBACZ;wBAED,OAAO,KAAK,CAAA;qBACb;iBACF;aACF,CAAC,CACH,CAAA;SACF;QAED,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;YAC5B,OAAO,CAAC,IAAI,CACV,IAAI,MAAM,CAAC;gBACT,GAAG,EAAE,IAAI,SAAS,CAAC,iBAAiB,CAAC;gBACrC,KAAK,EAAE;oBACL,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK;wBAC9B,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;wBACtB,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;wBAC3B,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAA;wBAE3B,IAAI,KAAK,EAAE;4BACT,OAAO,KAAK,CAAA;yBACb;wBAED,IAAI,WAAW,GAAG,EAAE,CAAA;wBAEpB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI;4BACxB,WAAW,IAAI,IAAI,CAAC,WAAW,CAAA;yBAChC,CAAC,CAAA;wBAEF,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;6BAC3B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAA;wBAE1D,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,EAAE;4BACzB,OAAO,KAAK,CAAA;yBACb;wBAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE;4BACtC,IAAI,EAAE,IAAI,CAAC,IAAI;yBAChB,CAAC,CAAA;wBAEF,OAAO,IAAI,CAAA;qBACZ;iBACF;aACF,CAAC,CACH,CAAA;SACF;QAED,OAAO,OAAO,CAAA;KACf;CACF;;;;"}
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)\n const newLinkText = newState.doc.textBetween(newMark.from, newMark.to)\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 find(textBlock.node.textContent)\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":"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"}
@@ -1,17 +1,136 @@
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);
34
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to);
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
+ linkifyjs.find(textBlock.node.textContent)
47
+ .filter(link => link.isLink)
48
+ // calculate link position
49
+ .map(link => ({
50
+ ...link,
51
+ from: textBlock.pos + link.start + 1,
52
+ to: textBlock.pos + link.end + 1,
53
+ }))
54
+ // check if link is within the changed range
55
+ .filter(link => {
56
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to;
57
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to;
58
+ return fromIsInRange || toIsInRange;
59
+ })
60
+ // add link mark
61
+ .forEach(link => {
62
+ tr.addMark(link.from, link.to, options.type.create({
63
+ href: link.href,
64
+ }));
65
+ });
66
+ });
67
+ });
68
+ if (!tr.steps.length) {
69
+ return;
70
+ }
71
+ return tr;
72
+ },
73
+ });
74
+ }
75
+
76
+ function clickHandler(options) {
77
+ return new prosemirrorState.Plugin({
78
+ key: new prosemirrorState.PluginKey('handleClickLink'),
79
+ props: {
80
+ handleClick: (view, pos, event) => {
81
+ var _a;
82
+ const attrs = core.getAttributes(view.state, options.type.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
+ },
91
+ });
92
+ }
93
+
94
+ function pasteHandler(options) {
95
+ return new prosemirrorState.Plugin({
96
+ key: new prosemirrorState.PluginKey('handlePasteLink'),
97
+ props: {
98
+ handlePaste: (view, event, slice) => {
99
+ const { state } = view;
100
+ const { selection } = state;
101
+ const { empty } = selection;
102
+ if (empty) {
103
+ return false;
104
+ }
105
+ let textContent = '';
106
+ slice.content.forEach(node => {
107
+ textContent += node.textContent;
108
+ });
109
+ const link = linkifyjs.find(textContent).find(item => item.isLink && item.value === textContent);
110
+ if (!textContent || !link) {
111
+ return false;
112
+ }
113
+ options.editor.commands.setMark(options.type, {
114
+ href: link.href,
115
+ });
116
+ return true;
117
+ },
118
+ },
119
+ });
120
+ }
6
121
 
7
122
  const Link = core.Mark.create({
8
123
  name: 'link',
9
124
  priority: 1000,
10
- inclusive: false,
125
+ keepOnSplit: false,
126
+ inclusive() {
127
+ return this.options.autolink;
128
+ },
11
129
  addOptions() {
12
130
  return {
13
131
  openOnClick: true,
14
132
  linkOnPaste: true,
133
+ autolink: true,
15
134
  HTMLAttributes: {
16
135
  target: '_blank',
17
136
  rel: 'noopener noreferrer nofollow',
@@ -34,7 +153,11 @@
34
153
  ];
35
154
  },
36
155
  renderHTML({ HTMLAttributes }) {
37
- return ['a', core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
156
+ return [
157
+ 'a',
158
+ core.mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
159
+ 0,
160
+ ];
38
161
  },
39
162
  addCommands() {
40
163
  return {
@@ -71,49 +194,20 @@
71
194
  },
72
195
  addProseMirrorPlugins() {
73
196
  const plugins = [];
197
+ if (this.options.autolink) {
198
+ plugins.push(autolink({
199
+ type: this.type,
200
+ }));
201
+ }
74
202
  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
- },
203
+ plugins.push(clickHandler({
204
+ type: this.type,
89
205
  }));
90
206
  }
91
207
  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
- },
208
+ plugins.push(pasteHandler({
209
+ editor: this.editor,
210
+ type: this.type,
117
211
  }));
118
212
  }
119
213
  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)\n const newLinkText = newState.doc.textBetween(newMark.from, newMark.to)\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 find(textBlock.node.textContent)\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.30",
4
+ "version": "2.0.0-beta.31",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
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": "6360278660d9c1e256975699d911a55dfa943d5d"
36
+ "gitHead": "4e1a50250bc5d3ef916326e5bed30c93448284b3"
36
37
  }
@@ -0,0 +1,92 @@
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)
47
+ const newLinkText = newState.doc.textBetween(newMark.from, newMark.to)
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
+ find(textBlock.node.textContent)
62
+ .filter(link => link.isLink)
63
+ // calculate link position
64
+ .map(link => ({
65
+ ...link,
66
+ from: textBlock.pos + link.start + 1,
67
+ to: textBlock.pos + link.end + 1,
68
+ }))
69
+ // check if link is within the changed range
70
+ .filter(link => {
71
+ const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to
72
+ const toIsInRange = newRange.to >= link.from && newRange.to <= link.to
73
+
74
+ return fromIsInRange || toIsInRange
75
+ })
76
+ // add link mark
77
+ .forEach(link => {
78
+ tr.addMark(link.from, link.to, options.type.create({
79
+ href: link.href,
80
+ }))
81
+ })
82
+ })
83
+ })
84
+
85
+ if (!tr.steps.length) {
86
+ return
87
+ }
88
+
89
+ return tr
90
+ },
91
+ })
92
+ }
@@ -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