@docs.plus/extension-hyperlink 1.5.2 → 4.3.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/CHANGELOG.md +362 -0
- package/LICENSE +21 -0
- package/README.md +263 -363
- package/dist/index.cjs +22 -712
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +255 -0
- package/dist/index.d.ts +255 -9
- package/dist/index.js +22 -707
- package/dist/index.js.map +1 -1
- package/dist/styles.css +323 -0
- package/package.json +78 -31
- package/dist/helpers/autoHyperlink.d.ts +0 -8
- package/dist/helpers/clickHandler.d.ts +0 -11
- package/dist/helpers/editHyperlink.d.ts +0 -9
- package/dist/helpers/pasteHandler.d.ts +0 -9
- package/dist/helpers/tippyHelper.d.ts +0 -29
- package/dist/hyperlink.d.ts +0 -77
- package/dist/index.umd.js +0 -713
- package/dist/index.umd.js.map +0 -1
- package/dist/modals/editeHyperlink.d.ts +0 -11
- package/dist/modals/icons.d.ts +0 -9
- package/dist/modals/previewHyperlink.d.ts +0 -11
- package/dist/modals/setHyperlink.d.ts +0 -14
- package/src/helpers/autoHyperlink.ts +0 -167
- package/src/helpers/clickHandler.ts +0 -80
- package/src/helpers/editHyperlink.ts +0 -59
- package/src/helpers/pasteHandler.ts +0 -46
- package/src/helpers/tippyHelper.ts +0 -136
- package/src/hyperlink.ts +0 -277
- package/src/index.ts +0 -17
- package/src/modals/editeHyperlink.ts +0 -99
- package/src/modals/icons.ts +0 -46
- package/src/modals/previewHyperlink.ts +0 -85
- package/src/modals/setHyperlink.ts +0 -97
package/README.md
CHANGED
|
@@ -1,454 +1,354 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @docs.plus/extension-hyperlink
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@docs.plus/extension-hyperlink)
|
|
4
4
|
[](https://npmcharts.com/compare/@docs.plus/extension-hyperlink)
|
|
5
5
|
[](https://www.npmjs.com/package/@docs.plus/extension-hyperlink)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> **v4 is a major rewrite.** Coming from v1.x? The API has been substantially
|
|
9
|
+
> redesigned (renamed options, renamed commands, kebab-case CSS classes,
|
|
10
|
+
> separate stylesheet, hardened XSS guards). Read the
|
|
11
|
+
> [v1 → v4 migration guide](./CHANGELOG.md#migrating-from-v1x-to-v4x)
|
|
12
|
+
> before upgrading.
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
A Tiptap extension for hyperlinks. Ships with optional prebuilt popovers for creating, previewing, and editing links, plus auto-linking, markdown input rules, and support for 50+ URL schemes (`mailto:`, `tel:`, `zoommtg:`, `vscode:`, `spotify:`, …).
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
<p align="center">
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/docs-plus/docs.plus/main/packages/extension-hyperlink/docs/screenshots/preview-dark.png">
|
|
19
|
+
<img alt="Hyperlink preview popover anchored to a link, with copy / edit / remove actions" width="640" src="https://raw.githubusercontent.com/docs-plus/docs.plus/main/packages/extension-hyperlink/docs/screenshots/preview-light.png">
|
|
20
|
+
</picture>
|
|
21
|
+
</p>
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
npm install @docs.plus/extension-hyperlink
|
|
15
|
-
````
|
|
16
|
-
|
|
17
|
-
## Settings
|
|
18
|
-
|
|
19
|
-
### protocols
|
|
23
|
+
## Install
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
```sh
|
|
26
|
+
npm install @docs.plus/extension-hyperlink
|
|
27
|
+
```
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
Peer dependencies:
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
})
|
|
29
|
-
````
|
|
31
|
+
```sh
|
|
32
|
+
npm install @tiptap/core @tiptap/pm
|
|
33
|
+
```
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
## Usage
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
```ts
|
|
38
|
+
import { useEditor } from '@tiptap/react'
|
|
39
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
40
|
+
import {
|
|
41
|
+
Hyperlink,
|
|
42
|
+
createHyperlinkPopover,
|
|
43
|
+
previewHyperlinkPopover
|
|
44
|
+
} from '@docs.plus/extension-hyperlink'
|
|
45
|
+
import '@docs.plus/extension-hyperlink/styles.css'
|
|
46
|
+
|
|
47
|
+
const editor = useEditor({
|
|
48
|
+
extensions: [
|
|
49
|
+
StarterKit.configure({ link: false }),
|
|
50
|
+
Hyperlink.configure({
|
|
51
|
+
popovers: {
|
|
52
|
+
previewHyperlink: previewHyperlinkPopover,
|
|
53
|
+
createHyperlink: createHyperlinkPopover
|
|
54
|
+
}
|
|
55
|
+
})
|
|
40
56
|
]
|
|
41
57
|
})
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
### autolink
|
|
45
|
-
|
|
46
|
-
If enabled, it adds links as you type.
|
|
58
|
+
```
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
The `styles.css` import and `popovers` options are optional — see [Popovers](#popovers) and [Styling](#styling) to bring your own UI.
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
Hyperlink.configure({
|
|
52
|
-
autolink: false,
|
|
53
|
-
})
|
|
54
|
-
````
|
|
62
|
+
## Options
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
| Option | Type | Default | Description |
|
|
65
|
+
| ---------------- | ----------------------------------------- | --------- | ----------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `autolink` | `boolean` | `true` | Convert URLs to links as you type. |
|
|
67
|
+
| `openOnClick` | `boolean` | `true` | Open links on click. Overridden by `popovers.previewHyperlink` when set. |
|
|
68
|
+
| `linkOnPaste` | `boolean` | `true` | Wrap the current selection in a link when a URL is pasted. |
|
|
69
|
+
| `protocols` | `Array<string \| LinkProtocolOptions>` | `[]` | Extra protocols to register with [linkifyjs](https://linkify.js.org). 50+ are already built in. |
|
|
70
|
+
| `HTMLAttributes` | `Record<string, any>` | `{ ... }` | Attributes applied to rendered `<a>` tags. Set a key to `null` to remove it. |
|
|
71
|
+
| `validate` | `(url: string) => boolean` | — | Reject auto-linked URLs when this returns `false`. |
|
|
72
|
+
| `popovers` | `{ previewHyperlink?, createHyperlink? }` | — | Factory functions that return `HTMLElement`. See [Popovers](#popovers). |
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
Example with options:
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
````js
|
|
76
|
+
```ts
|
|
63
77
|
Hyperlink.configure({
|
|
64
|
-
|
|
78
|
+
autolink: true,
|
|
79
|
+
openOnClick: true,
|
|
80
|
+
linkOnPaste: true,
|
|
81
|
+
protocols: ['ftp', { scheme: 'tel', optionalSlashes: true }],
|
|
82
|
+
HTMLAttributes: { rel: 'noopener noreferrer nofollow', target: '_blank' },
|
|
83
|
+
validate: (href) => /^https?:\/\//.test(href)
|
|
65
84
|
})
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### linkOnPaste
|
|
85
|
+
```
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
## Commands
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
| Command | Description |
|
|
90
|
+
| -------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `setHyperlink({ href, target? })` | Create a link on the current selection (or open the `createHyperlink` popover when configured). |
|
|
92
|
+
| `unsetHyperlink()` | Remove the link mark from the current selection. |
|
|
93
|
+
| `editHyperlink({ newURL?, newText? })` | Update the href and/or inner text of the link at the current selection. |
|
|
94
|
+
| `editHyperlinkHref(url)` | Shorthand for href-only edits. |
|
|
95
|
+
| `editHyperlinkText(text)` | Shorthand for text-only edits. |
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
````
|
|
97
|
+
```ts
|
|
98
|
+
editor.commands.setHyperlink({ href: 'https://example.com' })
|
|
99
|
+
editor.commands.editHyperlink({ newURL: 'https://new.example.com', newText: 'New label' })
|
|
100
|
+
editor.commands.unsetHyperlink()
|
|
79
101
|
|
|
80
|
-
|
|
102
|
+
editor.getAttributes('hyperlink').href // read current href
|
|
103
|
+
```
|
|
81
104
|
|
|
82
|
-
|
|
105
|
+
## Keyboard shortcuts
|
|
83
106
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class: 'my-custom-class',
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
````
|
|
107
|
+
| Shortcut | Action |
|
|
108
|
+
| -------- | ---------------------------------------------------- |
|
|
109
|
+
| `Mod-k` | Create or edit a hyperlink on the current selection. |
|
|
91
110
|
|
|
92
|
-
|
|
111
|
+
## Popovers
|
|
93
112
|
|
|
94
|
-
The
|
|
113
|
+
The extension is headless — you decide what the UI looks like. Three paths, from easiest to most flexible:
|
|
95
114
|
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
1. **Use the prebuilt popovers** (shown in [Usage](#usage)) and import `styles.css`.
|
|
116
|
+
2. **Restyle the prebuilt popovers** by overriding `--hl-*` custom properties (see [Styling](#styling)).
|
|
117
|
+
3. **Bring your own popover** — each popover is a factory that returns an `HTMLElement`. The extension handles positioning, outside-click dismissal, keyboard navigation, and cleanup.
|
|
98
118
|
|
|
99
119
|
<details>
|
|
100
|
-
<summary>
|
|
120
|
+
<summary><b>Custom <code>previewHyperlink</code></b></summary>
|
|
101
121
|
|
|
102
122
|
```ts
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
removeButton.addEventListener("click", () => {
|
|
130
|
-
options.tippy.hide();
|
|
131
|
-
return options.editor.chain().focus().unsetHyperlink().run();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
copyButton.addEventListener("click", () => {
|
|
135
|
-
options.tippy.hide();
|
|
136
|
-
navigator.clipboard.writeText(href);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
hyperlinkModal.append(newBubble, copyButton, removeButton);
|
|
140
|
-
|
|
141
|
-
return hyperlinkModal;
|
|
123
|
+
import {
|
|
124
|
+
Hyperlink,
|
|
125
|
+
hideCurrentToolbar,
|
|
126
|
+
type PreviewHyperlinkOptions
|
|
127
|
+
} from '@docs.plus/extension-hyperlink'
|
|
128
|
+
|
|
129
|
+
function previewHyperlink(options: PreviewHyperlinkOptions): HTMLElement {
|
|
130
|
+
const { editor, attrs } = options
|
|
131
|
+
const root = document.createElement('div')
|
|
132
|
+
|
|
133
|
+
const link = document.createElement('a')
|
|
134
|
+
link.href = attrs.href
|
|
135
|
+
link.textContent = attrs.href
|
|
136
|
+
link.target = '_blank'
|
|
137
|
+
link.rel = 'noreferrer'
|
|
138
|
+
|
|
139
|
+
const remove = document.createElement('button')
|
|
140
|
+
remove.textContent = 'Remove'
|
|
141
|
+
remove.addEventListener('click', () => {
|
|
142
|
+
hideCurrentToolbar()
|
|
143
|
+
editor.chain().focus().unsetHyperlink().run()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
root.append(link, remove)
|
|
147
|
+
return root
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
Hyperlink.configure({ popovers: { previewHyperlink } })
|
|
144
151
|
```
|
|
145
152
|
|
|
146
153
|
</details>
|
|
147
154
|
|
|
148
155
|
<details>
|
|
149
|
-
<summary>
|
|
156
|
+
<summary><b>Custom <code>createHyperlink</code></b></summary>
|
|
150
157
|
|
|
151
158
|
```ts
|
|
152
|
-
import {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
159
|
+
import {
|
|
160
|
+
Hyperlink,
|
|
161
|
+
hideCurrentToolbar,
|
|
162
|
+
normalizeHref,
|
|
163
|
+
validateURL,
|
|
164
|
+
type CreateHyperlinkOptions
|
|
165
|
+
} from '@docs.plus/extension-hyperlink'
|
|
166
|
+
|
|
167
|
+
function createHyperlink(options: CreateHyperlinkOptions): HTMLElement {
|
|
168
|
+
const { editor, extensionName, validate } = options
|
|
169
|
+
const form = document.createElement('form')
|
|
170
|
+
|
|
171
|
+
const input = document.createElement('input')
|
|
172
|
+
input.type = 'url'
|
|
173
|
+
input.placeholder = 'https://example.com'
|
|
174
|
+
|
|
175
|
+
const submit = document.createElement('button')
|
|
176
|
+
submit.type = 'submit'
|
|
177
|
+
submit.textContent = 'Apply'
|
|
178
|
+
|
|
179
|
+
form.append(input, submit)
|
|
180
|
+
form.addEventListener('submit', (event) => {
|
|
181
|
+
event.preventDefault()
|
|
182
|
+
const url = input.value.trim()
|
|
183
|
+
if (!validateURL(url, { customValidator: validate })) return
|
|
184
|
+
|
|
185
|
+
// Canonicalize before writing to the mark. `google.com` is a valid
|
|
186
|
+
// link, but stored verbatim it resolves against the host page's
|
|
187
|
+
// origin on click — `normalizeHref` prepends `https://` for bare
|
|
188
|
+
// domains and preserves anything already scheme-prefixed.
|
|
189
|
+
const href = normalizeHref(url)
|
|
190
|
+
|
|
191
|
+
hideCurrentToolbar()
|
|
192
|
+
editor.chain().setMark(extensionName, { href }).setMeta('preventAutolink', true).run()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return form
|
|
196
|
+
}
|
|
188
197
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
tooltip.update(options.editor.view, {
|
|
192
|
-
arrow: options.roundArrow,
|
|
193
|
-
});
|
|
198
|
+
Hyperlink.configure({ popovers: { createHyperlink } })
|
|
199
|
+
```
|
|
194
200
|
|
|
195
|
-
|
|
196
|
-
setTimeout(() => input.focus();, 100);
|
|
201
|
+
</details>
|
|
197
202
|
|
|
198
|
-
|
|
199
|
-
form.addEventListener("submit", (event) => {
|
|
200
|
-
event.preventDefault();
|
|
201
|
-
const url = input.value;
|
|
203
|
+
Popover content can control the floating toolbar via `hideCurrentToolbar()` and `updateCurrentToolbarPosition(ref)`, both exported from the package.
|
|
202
204
|
|
|
203
|
-
|
|
205
|
+
## Styling
|
|
204
206
|
|
|
205
|
-
|
|
206
|
-
.filter((link) => link.isLink)
|
|
207
|
-
.filter((link) => {
|
|
208
|
-
if (options.validate) {
|
|
209
|
-
return options.validate(link.value);
|
|
210
|
-
}
|
|
211
|
-
return true;
|
|
212
|
-
})
|
|
213
|
-
.at(0);
|
|
207
|
+
The prebuilt popovers ship with a small, framework-agnostic stylesheet. Import it once:
|
|
214
208
|
|
|
215
|
-
|
|
209
|
+
```ts
|
|
210
|
+
import '@docs.plus/extension-hyperlink/styles.css'
|
|
211
|
+
```
|
|
216
212
|
|
|
217
|
-
|
|
213
|
+
The CSS is opt-in — the extension's JavaScript never imports it, so custom UIs pay zero CSS cost.
|
|
218
214
|
|
|
219
|
-
|
|
220
|
-
.chain()
|
|
221
|
-
.setMark(options.extentionName, { href: sanitizeURL.href })
|
|
222
|
-
.setMeta("preventautohyperlink", true)
|
|
223
|
-
.run();
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
```
|
|
215
|
+
### Theming
|
|
227
216
|
|
|
228
|
-
|
|
217
|
+
Every visual token is a `--hl-*` custom property. Colors use [`light-dark()`](https://developer.mozilla.org/docs/Web/CSS/color_value/light-dark), so the popover follows the nearest ancestor's `color-scheme` — or the OS preference when none is set.
|
|
229
218
|
|
|
230
219
|
<details>
|
|
231
|
-
<summary>
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
justify-content: flex-end;
|
|
253
|
-
flex-direction: row-reverse;
|
|
254
|
-
a {
|
|
255
|
-
font-size: 0.9rem;
|
|
256
|
-
margin-right: 6px;
|
|
257
|
-
white-space: nowrap;
|
|
258
|
-
overflow: hidden;
|
|
259
|
-
text-overflow: ellipsis;
|
|
260
|
-
}
|
|
261
|
-
img {
|
|
262
|
-
width: 18px;
|
|
263
|
-
height: 18px;
|
|
264
|
-
border-radius: 50%;
|
|
265
|
-
margin-right: 8px;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
&__remove-button,
|
|
270
|
-
&__edit-button,
|
|
271
|
-
&__copy-button,
|
|
272
|
-
&__apply-button {
|
|
273
|
-
width: 30px;
|
|
274
|
-
height: 30px;
|
|
275
|
-
border-radius: 50%;
|
|
276
|
-
margin: 0 0.25rem;
|
|
277
|
-
display: flex;
|
|
278
|
-
align-items: center;
|
|
279
|
-
justify-content: center;
|
|
280
|
-
transition: background-color 0.1s ease-in-out;
|
|
281
|
-
&:hover {
|
|
282
|
-
background-color: #eee;
|
|
283
|
-
}
|
|
284
|
-
> svg {
|
|
285
|
-
width: 16px;
|
|
286
|
-
height: 16px;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
form {
|
|
291
|
-
display: flex;
|
|
292
|
-
align-items: flex-end;
|
|
293
|
-
width: 100%;
|
|
294
|
-
input {
|
|
295
|
-
border: 1px solid #dadce0;
|
|
296
|
-
border-radius: 6px;
|
|
297
|
-
padding: 0.4rem 0.8rem;
|
|
298
|
-
margin-bottom: 0.2rem;
|
|
299
|
-
width: 100%;
|
|
300
|
-
&:last-of-type {
|
|
301
|
-
margin-bottom: 0;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
.hyperlink-set-modal__buttons-wrapper,
|
|
305
|
-
.hyperlink-edit-modal__buttons-wrapper {
|
|
306
|
-
margin-left: 8px;
|
|
307
|
-
button {
|
|
308
|
-
border-radius: 6px;
|
|
309
|
-
padding: 4px 14px;
|
|
310
|
-
width: 70px;
|
|
311
|
-
margin-bottom: 0.2rem;
|
|
312
|
-
color: #1a73e8;
|
|
313
|
-
&:hover {
|
|
314
|
-
background: rgba(26, 115, 232, 0.04);
|
|
315
|
-
color: #174ea6;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.tippy-svg-arrow {
|
|
323
|
-
top: -6px !important;
|
|
324
|
-
}
|
|
220
|
+
<summary>Default values</summary>
|
|
221
|
+
|
|
222
|
+
```css
|
|
223
|
+
:root {
|
|
224
|
+
--hl-bg: light-dark(#ffffff, #1f2937);
|
|
225
|
+
--hl-fg: light-dark(#111827, #f3f4f6);
|
|
226
|
+
--hl-muted: light-dark(#6b7280, #9ca3af);
|
|
227
|
+
--hl-border: light-dark(#e5e7eb, #374151);
|
|
228
|
+
--hl-hover: light-dark(#f3f4f6, #374151);
|
|
229
|
+
--hl-accent: light-dark(#2563eb, #60a5fa);
|
|
230
|
+
--hl-accent-fg: light-dark(#ffffff, #0b1220);
|
|
231
|
+
--hl-danger: light-dark(#dc2626, #f87171);
|
|
232
|
+
--hl-shadow:
|
|
233
|
+
0 1px 2px light-dark(rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.4)),
|
|
234
|
+
0 4px 12px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.5));
|
|
235
|
+
--hl-radius: 6px;
|
|
236
|
+
--hl-radius-sm: 4px;
|
|
237
|
+
--hl-font: system-ui, sans-serif;
|
|
238
|
+
--hl-font-size: 14px;
|
|
239
|
+
--hl-z-index: 9999;
|
|
240
|
+
--hl-transition: 120ms ease-in-out;
|
|
325
241
|
}
|
|
326
242
|
```
|
|
327
243
|
|
|
328
244
|
</details>
|
|
329
245
|
|
|
330
|
-
|
|
331
|
-
import {
|
|
332
|
-
Hyperlink,
|
|
333
|
-
previewHyperlinkModal,
|
|
334
|
-
setHyperlinkModal
|
|
335
|
-
} from "@docs.plus/extension-hyperlink";
|
|
336
|
-
|
|
337
|
-
Hyperlink.configure({
|
|
338
|
-
modals: {
|
|
339
|
-
previewHyperlink: (data) => {
|
|
340
|
-
return previewHyperlinkModal(data);
|
|
341
|
-
},
|
|
342
|
-
setHyperlink: (data) => {
|
|
343
|
-
return setHyperlinkModal(data);
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
})
|
|
347
|
-
````
|
|
348
|
-
|
|
349
|
-
> Note: The `previewHyperlinkModal` and `setHyperlinkModal` modals are prebuilt modal boxes. You can use them or create your own modal boxes. To learn how to create your own modal boxes, please check the [source code](https://github.com/HMarzban/extension-hyperlink/tree/main/packages/extension-hyperlink/src/modals)
|
|
350
|
-
|
|
351
|
-
<Su>
|
|
352
|
-
|
|
353
|
-
### Removing and overriding existing html attributes
|
|
246
|
+
#### Switching themes
|
|
354
247
|
|
|
355
|
-
|
|
248
|
+
If your app has a light/dark toggle, set `color-scheme` on the theme root — the popover follows along:
|
|
356
249
|
|
|
357
|
-
|
|
250
|
+
```css
|
|
251
|
+
html[data-theme='light'] {
|
|
252
|
+
color-scheme: light;
|
|
253
|
+
}
|
|
254
|
+
html[data-theme='dark'] {
|
|
255
|
+
color-scheme: dark;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
358
258
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
259
|
+
> **Bundler note.** Some CSS minifiers (lightningcss, for one) down-level `light-dark()` into a `@media (prefers-color-scheme: dark)` block, which pins colors to the OS preference. If that happens, re-declare the tokens on each branch — the attribute selector wins over the media query:
|
|
260
|
+
>
|
|
261
|
+
> ```css
|
|
262
|
+
> html[data-theme='light'] {
|
|
263
|
+
> --hl-bg: #ffffff;
|
|
264
|
+
> --hl-fg: #111827; /* … */
|
|
265
|
+
> }
|
|
266
|
+
> html[data-theme='dark'] {
|
|
267
|
+
> --hl-bg: #1f2937;
|
|
268
|
+
> --hl-fg: #f3f4f6; /* … */
|
|
269
|
+
> }
|
|
270
|
+
> ```
|
|
370
271
|
|
|
371
|
-
###
|
|
272
|
+
### Class names
|
|
372
273
|
|
|
373
|
-
|
|
274
|
+
Stable class names for consumers who want to write their own rules:
|
|
374
275
|
|
|
375
|
-
|
|
276
|
+
| Class | Element |
|
|
277
|
+
| ---------------------------- | ---------------------------------------------------- |
|
|
278
|
+
| `.floating-toolbar` | Toolbar container (adds `.visible` after mount). |
|
|
279
|
+
| `.floating-toolbar-arrow` | Arrow pointing at the reference element. |
|
|
280
|
+
| `.floating-toolbar-content` | Content wrapper inside the toolbar. |
|
|
281
|
+
| `.hyperlink-create-popover` | Create-link popover root. |
|
|
282
|
+
| `.hyperlink-preview-popover` | Preview popover root. |
|
|
283
|
+
| `.hyperlink-edit-popover` | Edit popover root. |
|
|
284
|
+
| `.inputs-wrapper` | Input group container (adds `.error` on validation). |
|
|
285
|
+
| `.buttons-wrapper` | Button group container. |
|
|
286
|
+
| `.search-icon` | Leading icon inside an input group. |
|
|
287
|
+
| `.error-message` | Validation error text (shown with `.show`). |
|
|
376
288
|
|
|
377
|
-
|
|
378
|
-
// only autolink urls with a protocol
|
|
379
|
-
Hyperlink.configure({
|
|
380
|
-
validate: href => /^https?:\/\//.test(href),
|
|
381
|
-
})
|
|
382
|
-
````
|
|
289
|
+
## URL handling
|
|
383
290
|
|
|
384
|
-
|
|
291
|
+
Every write path canonicalizes hrefs before storing them:
|
|
385
292
|
|
|
386
|
-
|
|
293
|
+
- **Bare domains** get `https://` prepended: `google.com` → `https://google.com`.
|
|
294
|
+
- **Explicit schemes** pass through as typed: `http://`, `ftp://`, `whatsapp://`, `mailto:`.
|
|
295
|
+
- **Protocol-relative URLs** stay as-is: `//example.com`.
|
|
387
296
|
|
|
388
|
-
|
|
297
|
+
The create popover, markdown input rule, autolink, and paste all produce the same `href` for the same input.
|
|
389
298
|
|
|
390
|
-
|
|
391
|
-
this.editor.commands.editHyperLinkText('New Text');
|
|
392
|
-
this.editor.commands.editHyperLinkHref('https://new-url.com');
|
|
393
|
-
this.editor.commands.editHyperlink({
|
|
394
|
-
newText: 'New Text',
|
|
395
|
-
newURL: 'https://new-url.com'
|
|
396
|
-
});
|
|
397
|
-
```
|
|
299
|
+
Validation also rejects scheme-prefixed typos with no real host. `https://googlecom` fails; `http://localhost`, `https://127.0.0.1`, and any registered custom scheme pass.
|
|
398
300
|
|
|
399
|
-
|
|
301
|
+
Two helpers are exported for custom popovers:
|
|
400
302
|
|
|
401
|
-
|
|
303
|
+
| Helper | Use when |
|
|
304
|
+
| ------------------------- | ---------------------------------------------------- |
|
|
305
|
+
| `normalizeHref(raw)` | You have a raw URL string. |
|
|
306
|
+
| `normalizeLinkifyHref(m)` | You have a linkifyjs match (e.g. from `find(text)`). |
|
|
402
307
|
|
|
403
|
-
|
|
404
|
-
this.editor.commands.setHyperlink({
|
|
405
|
-
href: '<https://example.com>'
|
|
406
|
-
});
|
|
308
|
+
## Security
|
|
407
309
|
|
|
408
|
-
|
|
409
|
-
href: '<https://example.com>',
|
|
410
|
-
target: '_blank'
|
|
411
|
-
});
|
|
310
|
+
Dangerous schemes (`javascript:`, `data:`, `vbscript:`) are blocked everywhere a link enters or leaves the editor:
|
|
412
311
|
|
|
413
|
-
|
|
414
|
-
|
|
312
|
+
- `parseHTML` strips matching `<a>` tags on document load and paste.
|
|
313
|
+
- The markdown input rule rejects `[text](javascript:…)` at creation.
|
|
314
|
+
- The click handler and preview popover short-circuit `window.open(…)` when the stored href matches.
|
|
415
315
|
|
|
416
|
-
|
|
316
|
+
On the read side, both handlers prefer the stored mark attribute over the DOM `link.href` property — so a relative href injected via `setContent` won't resolve against the host page's origin.
|
|
417
317
|
|
|
418
|
-
|
|
318
|
+
The shared `DANGEROUS_SCHEME_RE` regex is exported for custom popovers that need the same check.
|
|
419
319
|
|
|
420
|
-
|
|
421
|
-
this.editor.commands.unsetHyperlink();
|
|
422
|
-
````
|
|
423
|
-
|
|
424
|
-
## Keyboard shortcuts
|
|
320
|
+
## TypeScript
|
|
425
321
|
|
|
426
|
-
|
|
427
|
-
This extension doesn’t bind a specific keyboard shortcut. You would probably open your custom UI on `Mod-k` though.
|
|
322
|
+
Definitions are bundled. Public types: `HyperlinkOptions`, `HyperlinkAttributes`, `PreviewHyperlinkOptions`, `CreateHyperlinkOptions`, `EditHyperlinkModalOptions`, `LinkProtocolOptions`, `FloatingToolbarOptions`, `FloatingToolbarInstance`, `LinkifyMatchLike`.
|
|
428
323
|
|
|
429
|
-
##
|
|
324
|
+
## Testing
|
|
430
325
|
|
|
431
|
-
|
|
326
|
+
Cypress runs against the built `dist/` output in a tiny `Bun.serve` playground — the same bytes an npm consumer installs.
|
|
432
327
|
|
|
433
|
-
```
|
|
434
|
-
|
|
328
|
+
```sh
|
|
329
|
+
bun run test # build + run Cypress headless
|
|
330
|
+
bun run test:open # build + open the Cypress runner
|
|
331
|
+
bun run playground # just the playground at http://127.0.0.1:5173
|
|
332
|
+
bun run docs:screenshots # regenerate README hero PNGs in docs/screenshots/
|
|
435
333
|
```
|
|
436
334
|
|
|
437
|
-
|
|
335
|
+
Append `?popover=custom` to the playground URL to swap in minimal BYO factories — that's how `custom-popover.cy.ts` exercises the public contract.
|
|
438
336
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
337
|
+
| Spec | Covers |
|
|
338
|
+
| ---------------------- | ---------------------------------------------------------------------- |
|
|
339
|
+
| `create.cy.ts` | `Mod+K`, Apply state, href canonicalization, host validation, dismiss. |
|
|
340
|
+
| `preview-edit.cy.ts` | Click → preview → edit/copy/remove, Back, Escape lifecycle. |
|
|
341
|
+
| `autolink.cy.ts` | Autolink, paste, and `mailto:` all produce the same `href`. |
|
|
342
|
+
| `xss-guards.cy.ts` | `javascript:` / `data:` / `vbscript:` blocked at every entry point. |
|
|
343
|
+
| `styling.cy.ts` | `styles.css` loaded, `--hl-*` tokens resolve, `light-dark()` branch. |
|
|
344
|
+
| `custom-popover.cy.ts` | BYO factory contract — options, mount lifecycle, exports. |
|
|
444
345
|
|
|
445
|
-
|
|
346
|
+
The suite also runs from the repo root via `bun run test:all`, alongside the Jest and webapp Cypress suites.
|
|
446
347
|
|
|
447
|
-
|
|
348
|
+
## License
|
|
448
349
|
|
|
449
|
-
|
|
450
|
-
> Please note: We're not affiliated with Tiptap, but we believe in recognizing foundational work.
|
|
350
|
+
MIT
|
|
451
351
|
|
|
452
|
-
|
|
352
|
+
## Credits
|
|
453
353
|
|
|
454
|
-
|
|
354
|
+
Inspired by Tiptap's [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/main/packages/extension-link). Part of [docs.plus](https://github.com/docs-plus/docs.plus).
|