@portabletext/plugin-paste-link 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 - 2026 Sanity.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# `@portabletext/plugin-paste-link`
|
|
2
|
+
|
|
3
|
+
> Allows pasting links in the Portable Text Editor
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @portabletext/plugin-paste-link
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Import the `PasteLinkPlugin` React component and place it inside the `EditorProvider`:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import {
|
|
17
|
+
defineSchema,
|
|
18
|
+
EditorProvider,
|
|
19
|
+
PortableTextEditable,
|
|
20
|
+
} from '@portabletext/editor'
|
|
21
|
+
import {PasteLinkPlugin} from '@portabletext/plugin-paste-link'
|
|
22
|
+
|
|
23
|
+
const schemaDefinition = defineSchema({
|
|
24
|
+
annotations: [{name: 'link', fields: [{name: 'href', type: 'string'}]}],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function App() {
|
|
28
|
+
return (
|
|
29
|
+
<EditorProvider initialConfig={{schemaDefinition}}>
|
|
30
|
+
<PortableTextEditable />
|
|
31
|
+
<PasteLinkPlugin />
|
|
32
|
+
</EditorProvider>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
By default, the plugin looks for a `link` annotation with an `href` field of type `string`.
|
|
38
|
+
|
|
39
|
+
### Controlling when the plugin runs
|
|
40
|
+
|
|
41
|
+
Use the `guard` prop to control when the paste link behavior runs. Return `false` to skip the behavior and fall through to default paste handling:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import {getActiveStyle} from '@portabletext/editor/selectors'
|
|
45
|
+
|
|
46
|
+
;<PasteLinkPlugin
|
|
47
|
+
guard={({snapshot}) => {
|
|
48
|
+
// Skip paste-link on h1 blocks (e.g., document titles)
|
|
49
|
+
return getActiveStyle(snapshot) !== 'h1'
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Customizing the link annotation
|
|
55
|
+
|
|
56
|
+
You can customize the link annotation with the `link` prop:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<PasteLinkPlugin
|
|
60
|
+
link={({context, value}) => {
|
|
61
|
+
const schemaType = context.schema.annotations.find(
|
|
62
|
+
(annotation) => annotation.name === 'customLink',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (!schemaType) return undefined
|
|
66
|
+
|
|
67
|
+
return {_type: schemaType.name, url: value.href}
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Behaviors
|
|
73
|
+
|
|
74
|
+
### Paste URL on selected text
|
|
75
|
+
|
|
76
|
+
When text is selected and a URL is pasted, the plugin adds a link annotation to the selection.
|
|
77
|
+
|
|
78
|
+
### Paste URL on existing link
|
|
79
|
+
|
|
80
|
+
When text with an existing link annotation is selected and a URL is pasted, the plugin replaces the existing link with a new one containing the pasted URL.
|
|
81
|
+
|
|
82
|
+
### Paste URL at caret
|
|
83
|
+
|
|
84
|
+
When the selection is collapsed and a URL is pasted, the plugin inserts the URL text with a link annotation. Existing decorators (bold, italic, etc.) are preserved.
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
### `PasteLinkPlugin`
|
|
89
|
+
|
|
90
|
+
React component that registers paste behaviors for handling URLs.
|
|
91
|
+
|
|
92
|
+
#### Props
|
|
93
|
+
|
|
94
|
+
- `guard` (optional): A `PasteLinkGuard` function that controls when the paste link behavior runs.
|
|
95
|
+
- Parameters: `{snapshot, event, dom}` - standard behavior guard parameters
|
|
96
|
+
- Returns: `true` to allow the behavior, `false` to skip and fall through to default paste handling.
|
|
97
|
+
- Use this to disable paste-link behavior in certain contexts (e.g., title blocks, code blocks).
|
|
98
|
+
|
|
99
|
+
- `link` (optional): A `LinkMatcher` function that converts a pasted URL into a link annotation.
|
|
100
|
+
- Parameters:
|
|
101
|
+
- `context`: Contains `schema` and `keyGenerator` from the editor context
|
|
102
|
+
- `value`: Contains `href` (the pasted URL string)
|
|
103
|
+
- Returns: An object with `_type` (annotation type name), optional `_key`, and any additional properties. Return `undefined` to skip the behavior.
|
|
104
|
+
- Default: Looks for a `link` annotation with an `href` field of type `string`.
|
|
105
|
+
|
|
106
|
+
### Types
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
type PasteLinkGuard = BehaviorGuard<
|
|
110
|
+
Extract<NativeBehaviorEvent, {type: 'clipboard.paste'}>,
|
|
111
|
+
true
|
|
112
|
+
>
|
|
113
|
+
|
|
114
|
+
type LinkMatcher = (params: {
|
|
115
|
+
context: LinkMatcherContext
|
|
116
|
+
value: LinkMatcherValue
|
|
117
|
+
}) => LinkMatcherResult | undefined
|
|
118
|
+
|
|
119
|
+
type LinkMatcherContext = Pick<EditorContext, 'schema' | 'keyGenerator'>
|
|
120
|
+
type LinkMatcherValue = {href: string}
|
|
121
|
+
type LinkMatcherResult = {
|
|
122
|
+
_type: string
|
|
123
|
+
_key?: string
|
|
124
|
+
[other: string]: unknown
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Supported protocols
|
|
129
|
+
|
|
130
|
+
- `http:`
|
|
131
|
+
- `https:`
|
|
132
|
+
- `mailto:`
|
|
133
|
+
- `tel:`
|
|
134
|
+
|
|
135
|
+
URLs with other protocols are pasted as plain text.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type {EditorContext} from '@portabletext/editor'
|
|
2
|
+
import {
|
|
3
|
+
BehaviorGuard,
|
|
4
|
+
NativeBehaviorEvent,
|
|
5
|
+
} from '@portabletext/editor/behaviors'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Function that converts a pasted URL into a link annotation.
|
|
9
|
+
* Return `undefined` to skip handling.
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export declare type LinkMatcher = (params: {
|
|
13
|
+
context: LinkMatcherContext
|
|
14
|
+
value: LinkMatcherValue
|
|
15
|
+
}) => LinkMatcherResult | undefined
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Context provided to link matchers.
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export declare type LinkMatcherContext = Pick<
|
|
22
|
+
EditorContext,
|
|
23
|
+
'schema' | 'keyGenerator'
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Object returned by link matchers.
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export declare type LinkMatcherResult = {
|
|
31
|
+
_type: string
|
|
32
|
+
_key?: string
|
|
33
|
+
[other: string]: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Value provided to link matchers.
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export declare type LinkMatcherValue = {
|
|
41
|
+
href: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Guard function that controls when the paste link behavior runs.
|
|
46
|
+
* Return `false` to skip the behavior and fall through to default paste handling.
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export declare type PasteLinkGuard = BehaviorGuard<
|
|
50
|
+
Extract<
|
|
51
|
+
NativeBehaviorEvent,
|
|
52
|
+
{
|
|
53
|
+
type: 'clipboard.paste'
|
|
54
|
+
}
|
|
55
|
+
>,
|
|
56
|
+
true
|
|
57
|
+
>
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Plugin that handles pasting URLs in the editor.
|
|
61
|
+
*
|
|
62
|
+
* When text is selected and a URL is pasted, adds a link annotation to the selection.
|
|
63
|
+
* When the caret is collapsed (no selection) and a URL is pasted, inserts the URL as text with a link annotation.
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
export declare function PasteLinkPlugin({
|
|
68
|
+
guard,
|
|
69
|
+
link,
|
|
70
|
+
}: PasteLinkPluginProps): null
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
export declare type PasteLinkPluginProps = {
|
|
76
|
+
guard?: PasteLinkGuard
|
|
77
|
+
link?: LinkMatcher
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { c } from "react/compiler-runtime";
|
|
2
|
+
import { useEditor } from "@portabletext/editor";
|
|
3
|
+
import { defineBehavior, raise } from "@portabletext/editor/behaviors";
|
|
4
|
+
import * as selectors from "@portabletext/editor/selectors";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
function looksLikeUrl(text) {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(text);
|
|
9
|
+
return sensibleProtocols.includes(url.protocol);
|
|
10
|
+
} catch {
|
|
11
|
+
return !1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const sensibleProtocols = ["http:", "https:", "mailto:", "tel:"], defaultLinkMatcher = ({
|
|
15
|
+
context,
|
|
16
|
+
value
|
|
17
|
+
}) => {
|
|
18
|
+
const schemaType = context.schema.annotations.find((annotation) => annotation.name === "link"), hrefField = schemaType?.fields.find((field) => field.name === "href" && field.type === "string");
|
|
19
|
+
if (!(!schemaType || !hrefField))
|
|
20
|
+
return {
|
|
21
|
+
_type: schemaType.name,
|
|
22
|
+
[hrefField.name]: value.href
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
function PasteLinkPlugin(t0) {
|
|
26
|
+
const $ = c(5), {
|
|
27
|
+
guard,
|
|
28
|
+
link: t1
|
|
29
|
+
} = t0, link = t1 === void 0 ? defaultLinkMatcher : t1, editor = useEditor();
|
|
30
|
+
let t2, t3;
|
|
31
|
+
return $[0] !== editor || $[1] !== guard || $[2] !== link ? (t2 = () => {
|
|
32
|
+
const unregisterBehaviors = createPasteLinkBehaviors({
|
|
33
|
+
guard,
|
|
34
|
+
link
|
|
35
|
+
}).map((behavior) => editor.registerBehavior({
|
|
36
|
+
behavior
|
|
37
|
+
}));
|
|
38
|
+
return () => {
|
|
39
|
+
for (const unregisterBehavior of unregisterBehaviors)
|
|
40
|
+
unregisterBehavior();
|
|
41
|
+
};
|
|
42
|
+
}, t3 = [editor, guard, link], $[0] = editor, $[1] = guard, $[2] = link, $[3] = t2, $[4] = t3) : (t2 = $[3], t3 = $[4]), useEffect(t2, t3), null;
|
|
43
|
+
}
|
|
44
|
+
function createPasteLinkBehaviors(config) {
|
|
45
|
+
const pasteLinkOnSelection = defineBehavior({
|
|
46
|
+
on: "clipboard.paste",
|
|
47
|
+
guard: (guardParams) => {
|
|
48
|
+
if (config.guard && config.guard(guardParams) === !1)
|
|
49
|
+
return !1;
|
|
50
|
+
const {
|
|
51
|
+
snapshot,
|
|
52
|
+
event
|
|
53
|
+
} = guardParams;
|
|
54
|
+
if (selectors.isSelectionCollapsed(snapshot))
|
|
55
|
+
return !1;
|
|
56
|
+
const text = event.originEvent.dataTransfer.getData("text/plain"), href = looksLikeUrl(text) ? text : void 0;
|
|
57
|
+
if (!href)
|
|
58
|
+
return !1;
|
|
59
|
+
const result = config.link({
|
|
60
|
+
context: {
|
|
61
|
+
schema: snapshot.context.schema,
|
|
62
|
+
keyGenerator: snapshot.context.keyGenerator
|
|
63
|
+
},
|
|
64
|
+
value: {
|
|
65
|
+
href
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (!result)
|
|
69
|
+
return !1;
|
|
70
|
+
const {
|
|
71
|
+
_type,
|
|
72
|
+
_key,
|
|
73
|
+
...value
|
|
74
|
+
} = result;
|
|
75
|
+
return {
|
|
76
|
+
annotation: {
|
|
77
|
+
name: _type,
|
|
78
|
+
_key,
|
|
79
|
+
value
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
actions: [(_, {
|
|
84
|
+
annotation
|
|
85
|
+
}) => [raise({
|
|
86
|
+
type: "annotation.add",
|
|
87
|
+
annotation
|
|
88
|
+
})]]
|
|
89
|
+
}), pasteLinkAtCaret = defineBehavior({
|
|
90
|
+
on: "clipboard.paste",
|
|
91
|
+
guard: (guardParams) => {
|
|
92
|
+
if (config.guard && config.guard(guardParams) === !1)
|
|
93
|
+
return !1;
|
|
94
|
+
const {
|
|
95
|
+
snapshot,
|
|
96
|
+
event
|
|
97
|
+
} = guardParams, focusTextBlock = selectors.getFocusTextBlock(snapshot), selectionCollapsed = selectors.isSelectionCollapsed(snapshot);
|
|
98
|
+
if (!focusTextBlock || !selectionCollapsed)
|
|
99
|
+
return !1;
|
|
100
|
+
const text = event.originEvent.dataTransfer.getData("text/plain"), href = looksLikeUrl(text) ? text : void 0;
|
|
101
|
+
if (!href)
|
|
102
|
+
return !1;
|
|
103
|
+
const result = config.link({
|
|
104
|
+
context: {
|
|
105
|
+
schema: snapshot.context.schema,
|
|
106
|
+
keyGenerator: snapshot.context.keyGenerator
|
|
107
|
+
},
|
|
108
|
+
value: {
|
|
109
|
+
href
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (!result)
|
|
113
|
+
return !1;
|
|
114
|
+
const {
|
|
115
|
+
_type,
|
|
116
|
+
_key,
|
|
117
|
+
...value
|
|
118
|
+
} = result, markState = selectors.getMarkState(snapshot), decoratorNames = snapshot.context.schema.decorators.map((decorator) => decorator.name), activeDecorators = (markState?.marks ?? []).filter((mark) => decoratorNames.includes(mark)), markDefKey = _key ?? snapshot.context.keyGenerator(), markDef = {
|
|
119
|
+
_type,
|
|
120
|
+
_key: markDefKey,
|
|
121
|
+
...value
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
focusTextBlock,
|
|
125
|
+
markDef,
|
|
126
|
+
markDefKey,
|
|
127
|
+
href,
|
|
128
|
+
activeDecorators
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
actions: [({
|
|
132
|
+
snapshot
|
|
133
|
+
}, {
|
|
134
|
+
focusTextBlock,
|
|
135
|
+
markDef,
|
|
136
|
+
markDefKey,
|
|
137
|
+
href,
|
|
138
|
+
activeDecorators
|
|
139
|
+
}) => [raise({
|
|
140
|
+
type: "block.set",
|
|
141
|
+
at: focusTextBlock.path,
|
|
142
|
+
props: {
|
|
143
|
+
markDefs: [...focusTextBlock.node.markDefs ?? [], markDef]
|
|
144
|
+
}
|
|
145
|
+
}), raise({
|
|
146
|
+
type: "insert.child",
|
|
147
|
+
child: {
|
|
148
|
+
_type: snapshot.context.schema.span.name,
|
|
149
|
+
text: href,
|
|
150
|
+
marks: [...activeDecorators, markDefKey]
|
|
151
|
+
}
|
|
152
|
+
})]]
|
|
153
|
+
});
|
|
154
|
+
return [pasteLinkOnSelection, pasteLinkAtCaret];
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
PasteLinkPlugin
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/looks-like-url.ts","../src/plugin.paste-link.tsx"],"sourcesContent":["export function looksLikeUrl(text: string) {\n try {\n const url = new URL(text)\n return sensibleProtocols.includes(url.protocol)\n } catch {\n return false\n }\n}\n\nconst sensibleProtocols = ['http:', 'https:', 'mailto:', 'tel:']\n","import type {EditorContext} from '@portabletext/editor'\nimport {useEditor} from '@portabletext/editor'\nimport {\n defineBehavior,\n raise,\n type BehaviorGuard,\n type NativeBehaviorEvent,\n} from '@portabletext/editor/behaviors'\nimport * as selectors from '@portabletext/editor/selectors'\nimport {useEffect} from 'react'\nimport {looksLikeUrl} from './looks-like-url'\n\n/**\n * Guard function that controls when the paste link behavior runs.\n * Return `false` to skip the behavior and fall through to default paste handling.\n * @public\n */\nexport type PasteLinkGuard = BehaviorGuard<\n Extract<NativeBehaviorEvent, {type: 'clipboard.paste'}>,\n true\n>\n\n/**\n * Context provided to link matchers.\n * @public\n */\nexport type LinkMatcherContext = Pick<EditorContext, 'schema' | 'keyGenerator'>\n\n/**\n * Value provided to link matchers.\n * @public\n */\nexport type LinkMatcherValue = {href: string}\n\n/**\n * Object returned by link matchers.\n * @public\n */\nexport type LinkMatcherResult = {\n _type: string\n _key?: string\n [other: string]: unknown\n}\n\n/**\n * Function that converts a pasted URL into a link annotation.\n * Return `undefined` to skip handling.\n * @public\n */\nexport type LinkMatcher = (params: {\n context: LinkMatcherContext\n value: LinkMatcherValue\n}) => LinkMatcherResult | undefined\n\n/**\n * @public\n */\nexport type PasteLinkPluginProps = {\n guard?: PasteLinkGuard\n link?: LinkMatcher\n}\n\nconst defaultLinkMatcher: LinkMatcher = ({context, value}) => {\n const schemaType = context.schema.annotations.find(\n (annotation) => annotation.name === 'link',\n )\n const hrefField = schemaType?.fields.find(\n (field) => field.name === 'href' && field.type === 'string',\n )\n\n if (!schemaType || !hrefField) {\n return undefined\n }\n\n return {\n _type: schemaType.name,\n [hrefField.name]: value.href,\n }\n}\n\n/**\n * Plugin that handles pasting URLs in the editor.\n *\n * When text is selected and a URL is pasted, adds a link annotation to the selection.\n * When the caret is collapsed (no selection) and a URL is pasted, inserts the URL as text with a link annotation.\n *\n * @public\n */\nexport function PasteLinkPlugin({\n guard,\n link = defaultLinkMatcher,\n}: PasteLinkPluginProps) {\n const editor = useEditor()\n\n useEffect(() => {\n const behaviors = createPasteLinkBehaviors({guard, link})\n const unregisterBehaviors = behaviors.map((behavior) =>\n editor.registerBehavior({behavior}),\n )\n\n return () => {\n for (const unregisterBehavior of unregisterBehaviors) {\n unregisterBehavior()\n }\n }\n }, [editor, guard, link])\n\n return null\n}\n\nfunction createPasteLinkBehaviors(\n config: Required<Pick<PasteLinkPluginProps, 'link'>> &\n Pick<PasteLinkPluginProps, 'guard'>,\n) {\n /**\n * When text is selected and a URL is pasted, add a link annotation to the\n * selection. If the selection already has a link annotation, the core\n * `preventOverlappingAnnotations` behavior will remove it first.\n */\n const pasteLinkOnSelection = defineBehavior({\n on: 'clipboard.paste',\n guard: (guardParams) => {\n if (config.guard && config.guard(guardParams) === false) {\n return false\n }\n\n const {snapshot, event} = guardParams\n const selectionCollapsed = selectors.isSelectionCollapsed(snapshot)\n\n if (selectionCollapsed) {\n return false\n }\n\n const text = event.originEvent.dataTransfer.getData('text/plain')\n const href = looksLikeUrl(text) ? text : undefined\n\n if (!href) {\n return false\n }\n\n const result = config.link({\n context: {\n schema: snapshot.context.schema,\n keyGenerator: snapshot.context.keyGenerator,\n },\n value: {href},\n })\n\n if (!result) {\n return false\n }\n\n const {_type, _key, ...value} = result\n\n return {annotation: {name: _type, _key, value}}\n },\n actions: [\n (_, {annotation}) => [\n raise({\n type: 'annotation.add',\n annotation,\n }),\n ],\n ],\n })\n\n /**\n * When the caret is collapsed (no selection) and a URL is pasted, insert the\n * URL as text with a link annotation. Existing decorators (bold, italic,\n * etc.) are preserved.\n */\n const pasteLinkAtCaret = defineBehavior({\n on: 'clipboard.paste',\n guard: (guardParams) => {\n if (config.guard && config.guard(guardParams) === false) {\n return false\n }\n\n const {snapshot, event} = guardParams\n const focusTextBlock = selectors.getFocusTextBlock(snapshot)\n const selectionCollapsed = selectors.isSelectionCollapsed(snapshot)\n\n if (!focusTextBlock || !selectionCollapsed) {\n return false\n }\n\n const text = event.originEvent.dataTransfer.getData('text/plain')\n const href = looksLikeUrl(text) ? text : undefined\n\n if (!href) {\n return false\n }\n\n const result = config.link({\n context: {\n schema: snapshot.context.schema,\n keyGenerator: snapshot.context.keyGenerator,\n },\n value: {href},\n })\n\n if (!result) {\n return false\n }\n\n const {_type, _key, ...value} = result\n\n const markState = selectors.getMarkState(snapshot)\n const decoratorNames = snapshot.context.schema.decorators.map(\n (decorator) => decorator.name,\n )\n const activeDecorators = (markState?.marks ?? []).filter((mark) =>\n decoratorNames.includes(mark),\n )\n\n const markDefKey = _key ?? snapshot.context.keyGenerator()\n const markDef = {\n _type,\n _key: markDefKey,\n ...value,\n }\n\n return {\n focusTextBlock,\n markDef,\n markDefKey,\n href,\n activeDecorators,\n }\n },\n actions: [\n (\n {snapshot},\n {focusTextBlock, markDef, markDefKey, href, activeDecorators},\n ) => [\n raise({\n type: 'block.set',\n at: focusTextBlock.path,\n props: {\n markDefs: [...(focusTextBlock.node.markDefs ?? []), markDef],\n },\n }),\n raise({\n type: 'insert.child',\n child: {\n _type: snapshot.context.schema.span.name,\n text: href,\n marks: [...activeDecorators, markDefKey],\n },\n }),\n ],\n ],\n })\n\n return [pasteLinkOnSelection, pasteLinkAtCaret]\n}\n"],"names":["looksLikeUrl","text","url","URL","sensibleProtocols","includes","protocol","defaultLinkMatcher","context","value","schemaType","schema","annotations","find","annotation","name","hrefField","fields","field","type","_type","href","PasteLinkPlugin","t0","$","_c","guard","link","t1","undefined","editor","useEditor","t2","t3","unregisterBehaviors","createPasteLinkBehaviors","map","behavior","registerBehavior","unregisterBehavior","useEffect","config","pasteLinkOnSelection","defineBehavior","on","guardParams","snapshot","event","selectors","isSelectionCollapsed","originEvent","dataTransfer","getData","result","keyGenerator","_key","actions","_","raise","pasteLinkAtCaret","focusTextBlock","getFocusTextBlock","selectionCollapsed","markState","getMarkState","decoratorNames","decorators","decorator","activeDecorators","marks","filter","mark","markDefKey","markDef","at","path","props","markDefs","node","child","span"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@portabletext/plugin-paste-link",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Allows pasting links in the Portable Text Editor",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"portabletext",
|
|
7
|
+
"plugin",
|
|
8
|
+
"paste",
|
|
9
|
+
"link",
|
|
10
|
+
"url",
|
|
11
|
+
"behaviors"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://portabletext.org",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/portabletext/editor/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/portabletext/editor.git",
|
|
20
|
+
"directory": "packages/plugin-paste-link"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Sanity.io <hello@sanity.io>",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./dist/index.js",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@sanity/tsconfig": "^2.1.0",
|
|
37
|
+
"@types/react": "^19.2.7",
|
|
38
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
39
|
+
"@vitest/browser": "^4.0.16",
|
|
40
|
+
"@vitest/browser-playwright": "^4.0.16",
|
|
41
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
42
|
+
"eslint": "^9.39.1",
|
|
43
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
44
|
+
"react": "^19.2.3",
|
|
45
|
+
"typescript": "5.9.3",
|
|
46
|
+
"typescript-eslint": "^8.48.0",
|
|
47
|
+
"vitest": "^4.0.16",
|
|
48
|
+
"@portabletext/editor": "^4.3.6",
|
|
49
|
+
"racejar": "2.0.2"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^19.2",
|
|
53
|
+
"@portabletext/editor": "^4.3.6"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20.19 <22 || >=22.12"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "pkg-utils build --strict --check --clean",
|
|
63
|
+
"check:lint": "biome lint .",
|
|
64
|
+
"check:react-compiler": "eslint .",
|
|
65
|
+
"check:types": "tsc",
|
|
66
|
+
"check:types:watch": "tsc --watch",
|
|
67
|
+
"clean": "del .turbo && del dist && del node_modules",
|
|
68
|
+
"dev": "pkg-utils watch",
|
|
69
|
+
"lint:fix": "biome lint --write .",
|
|
70
|
+
"test:browser": "vitest run",
|
|
71
|
+
"test:browser:chromium": "vitest run --project \"browser (chromium)\"",
|
|
72
|
+
"test:browser:chromium:watch": "vitest watch --project \"browser (chromium)\"",
|
|
73
|
+
"test:browser:firefox": "vitest run --project \"browser (firefox)\"",
|
|
74
|
+
"test:browser:firefox:watch": "vitest watch --project \"browser (firefox)\"",
|
|
75
|
+
"test:browser:webkit": "vitest run --project \"browser (webkit)\"",
|
|
76
|
+
"test:browser:webkit:watch": "vitest watch --project \"browser (webkit)\"",
|
|
77
|
+
"test:unit": "vitest run --project unit"
|
|
78
|
+
}
|
|
79
|
+
}
|