@docs.plus/extension-hyperlink 1.5.1 → 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 -365
- 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,456 +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 @docsplus/extension-hyperlink
|
|
15
|
-
````
|
|
23
|
+
## Install
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Additional custom protocols you would like to be recognized as links.
|
|
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.
|
|
47
|
-
|
|
48
|
-
Default: `true`
|
|
58
|
+
```
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
Hyperlink.configure({
|
|
52
|
-
autolink: false,
|
|
53
|
-
})
|
|
54
|
-
````
|
|
60
|
+
The `styles.css` import and `popovers` options are optional — see [Popovers](#popovers) and [Styling](#styling) to bring your own UI.
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
## Options
|
|
57
63
|
|
|
58
|
-
|
|
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). |
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
Example with options:
|
|
61
75
|
|
|
62
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
182
197
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
form.append(inputsWrapper, buttonsWrapper);
|
|
198
|
+
Hyperlink.configure({ popovers: { createHyperlink } })
|
|
199
|
+
```
|
|
186
200
|
|
|
187
|
-
|
|
201
|
+
</details>
|
|
188
202
|
|
|
189
|
-
|
|
190
|
-
tippyModal.append(hyperlinkModal);
|
|
191
|
-
tooltip.update(options.editor.view, {
|
|
192
|
-
arrow: options.roundArrow,
|
|
193
|
-
});
|
|
203
|
+
Popover content can control the floating toolbar via `hideCurrentToolbar()` and `updateCurrentToolbarPosition(ref)`, both exported from the package.
|
|
194
204
|
|
|
195
|
-
|
|
196
|
-
setTimeout(() => input.focus();, 100);
|
|
205
|
+
## Styling
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
form.addEventListener("submit", (event) => {
|
|
200
|
-
event.preventDefault();
|
|
201
|
-
const url = input.value;
|
|
207
|
+
The prebuilt popovers ship with a small, framework-agnostic stylesheet. Import it once:
|
|
202
208
|
|
|
203
|
-
|
|
209
|
+
```ts
|
|
210
|
+
import '@docs.plus/extension-hyperlink/styles.css'
|
|
211
|
+
```
|
|
204
212
|
|
|
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);
|
|
213
|
+
The CSS is opt-in — the extension's JavaScript never imports it, so custom UIs pay zero CSS cost.
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
### Theming
|
|
216
216
|
|
|
217
|
-
|
|
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.
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
<details>
|
|
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;
|
|
225
241
|
}
|
|
226
242
|
```
|
|
227
243
|
|
|
228
244
|
</details>
|
|
229
245
|
|
|
246
|
+
#### Switching themes
|
|
230
247
|
|
|
231
|
-
|
|
232
|
-
<summary>The Hyperlink styles.scss</summary>
|
|
233
|
-
|
|
234
|
-
```scss
|
|
235
|
-
.tippy-box {
|
|
236
|
-
.hyperlink-preview-modal,
|
|
237
|
-
.hyperlink-set-modal,
|
|
238
|
-
.hyperlink-edit-modal {
|
|
239
|
-
background-color: #fff;
|
|
240
|
-
border-radius: 10px;
|
|
241
|
-
border: 1px solid #dadce0;
|
|
242
|
-
display: flex;
|
|
243
|
-
align-items: center;
|
|
244
|
-
justify-content: space-between;
|
|
245
|
-
padding: 6px 6px;
|
|
246
|
-
box-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
|
247
|
-
margin-top: -6px;
|
|
248
|
-
|
|
249
|
-
&__metadata {
|
|
250
|
-
width: 200px;
|
|
251
|
-
display: flex;
|
|
252
|
-
align-items: center;
|
|
253
|
-
justify-content: flex-end;
|
|
254
|
-
flex-direction: row-reverse;
|
|
255
|
-
a {
|
|
256
|
-
font-size: 0.9rem;
|
|
257
|
-
margin-right: 6px;
|
|
258
|
-
white-space: nowrap;
|
|
259
|
-
overflow: hidden;
|
|
260
|
-
text-overflow: ellipsis;
|
|
261
|
-
}
|
|
262
|
-
img {
|
|
263
|
-
width: 18px;
|
|
264
|
-
height: 18px;
|
|
265
|
-
border-radius: 50%;
|
|
266
|
-
margin-right: 8px;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
&__remove-button,
|
|
271
|
-
&__edit-button,
|
|
272
|
-
&__copy-button,
|
|
273
|
-
&__apply-button {
|
|
274
|
-
width: 30px;
|
|
275
|
-
height: 30px;
|
|
276
|
-
border-radius: 50%;
|
|
277
|
-
margin: 0 0.25rem;
|
|
278
|
-
display: flex;
|
|
279
|
-
align-items: center;
|
|
280
|
-
justify-content: center;
|
|
281
|
-
transition: background-color 0.1s ease-in-out;
|
|
282
|
-
&:hover {
|
|
283
|
-
background-color: #eee;
|
|
284
|
-
}
|
|
285
|
-
> svg {
|
|
286
|
-
width: 16px;
|
|
287
|
-
height: 16px;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
form {
|
|
292
|
-
display: flex;
|
|
293
|
-
align-items: flex-end;
|
|
294
|
-
width: 100%;
|
|
295
|
-
input {
|
|
296
|
-
border: 1px solid #dadce0;
|
|
297
|
-
border-radius: 6px;
|
|
298
|
-
padding: 0.4rem 0.8rem;
|
|
299
|
-
margin-bottom: 0.2rem;
|
|
300
|
-
width: 100%;
|
|
301
|
-
&:last-of-type {
|
|
302
|
-
margin-bottom: 0;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
.hyperlink-set-modal__buttons-wrapper,
|
|
306
|
-
.hyperlink-edit-modal__buttons-wrapper {
|
|
307
|
-
margin-left: 8px;
|
|
308
|
-
button {
|
|
309
|
-
border-radius: 6px;
|
|
310
|
-
padding: 4px 14px;
|
|
311
|
-
width: 70px;
|
|
312
|
-
margin-bottom: 0.2rem;
|
|
313
|
-
color: #1a73e8;
|
|
314
|
-
&:hover {
|
|
315
|
-
background: rgba(26, 115, 232, 0.04);
|
|
316
|
-
color: #174ea6;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
248
|
+
If your app has a light/dark toggle, set `color-scheme` on the theme root — the popover follows along:
|
|
322
249
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
250
|
+
```css
|
|
251
|
+
html[data-theme='light'] {
|
|
252
|
+
color-scheme: light;
|
|
253
|
+
}
|
|
254
|
+
html[data-theme='dark'] {
|
|
255
|
+
color-scheme: dark;
|
|
326
256
|
}
|
|
327
257
|
```
|
|
328
258
|
|
|
329
|
-
|
|
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
|
+
> ```
|
|
330
271
|
|
|
272
|
+
### Class names
|
|
331
273
|
|
|
332
|
-
|
|
333
|
-
import {
|
|
334
|
-
Hyperlink,
|
|
335
|
-
previewHyperlinkModal,
|
|
336
|
-
setHyperlinkModal
|
|
337
|
-
} from "@docs.plus/extension-hyperlink";
|
|
274
|
+
Stable class names for consumers who want to write their own rules:
|
|
338
275
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
> 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)
|
|
352
|
-
|
|
353
|
-
<Su>
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
### Removing and overriding existing html attributes
|
|
358
|
-
|
|
359
|
-
You can add `rel: null` to HTMLAttributes to remove the default `rel="noopener noreferrer nofollow"`. You can also override the default by using `rel: "your-value"`.
|
|
360
|
-
|
|
361
|
-
This can also be used to change the `target` from the default value of `_blank`.
|
|
362
|
-
|
|
363
|
-
````js
|
|
364
|
-
Hyperlink.configure({
|
|
365
|
-
HTMLAttributes: {
|
|
366
|
-
// Change rel to different value
|
|
367
|
-
// Allow search engines to follow links(remove nofollow)
|
|
368
|
-
rel: 'noopener noreferrer',
|
|
369
|
-
// Remove target entirely so links open in current tab
|
|
370
|
-
target: null,
|
|
371
|
-
},
|
|
372
|
-
})
|
|
373
|
-
````
|
|
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`). |
|
|
374
288
|
|
|
375
|
-
|
|
289
|
+
## URL handling
|
|
376
290
|
|
|
377
|
-
|
|
291
|
+
Every write path canonicalizes hrefs before storing them:
|
|
378
292
|
|
|
379
|
-
|
|
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`.
|
|
380
296
|
|
|
381
|
-
|
|
382
|
-
// only autolink urls with a protocol
|
|
383
|
-
Hyperlink.configure({
|
|
384
|
-
validate: href => /^https?:\/\//.test(href),
|
|
385
|
-
})
|
|
386
|
-
````
|
|
297
|
+
The create popover, markdown input rule, autolink, and paste all produce the same `href` for the same input.
|
|
387
298
|
|
|
388
|
-
|
|
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.
|
|
389
300
|
|
|
390
|
-
|
|
301
|
+
Two helpers are exported for custom popovers:
|
|
391
302
|
|
|
392
|
-
|
|
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)`). |
|
|
393
307
|
|
|
394
|
-
|
|
395
|
-
this.editor.commands.editHyperLinkText('New Text');
|
|
396
|
-
this.editor.commands.editHyperLinkHref('https://new-url.com');
|
|
397
|
-
this.editor.commands.editHyperlink({
|
|
398
|
-
newText: 'New Text',
|
|
399
|
-
newURL: 'https://new-url.com'
|
|
400
|
-
});
|
|
401
|
-
```
|
|
308
|
+
## Security
|
|
402
309
|
|
|
403
|
-
|
|
310
|
+
Dangerous schemes (`javascript:`, `data:`, `vbscript:`) are blocked everywhere a link enters or leaves the editor:
|
|
404
311
|
|
|
405
|
-
|
|
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.
|
|
406
315
|
|
|
407
|
-
|
|
408
|
-
this.editor.commands.setHyperlink({
|
|
409
|
-
href: '<https://example.com>'
|
|
410
|
-
});
|
|
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.
|
|
411
317
|
|
|
412
|
-
|
|
413
|
-
href: '<https://example.com>',
|
|
414
|
-
target: '_blank'
|
|
415
|
-
});
|
|
318
|
+
The shared `DANGEROUS_SCHEME_RE` regex is exported for custom popovers that need the same check.
|
|
416
319
|
|
|
417
|
-
|
|
418
|
-
````
|
|
320
|
+
## TypeScript
|
|
419
321
|
|
|
420
|
-
|
|
322
|
+
Definitions are bundled. Public types: `HyperlinkOptions`, `HyperlinkAttributes`, `PreviewHyperlinkOptions`, `CreateHyperlinkOptions`, `EditHyperlinkModalOptions`, `LinkProtocolOptions`, `FloatingToolbarOptions`, `FloatingToolbarInstance`, `LinkifyMatchLike`.
|
|
421
323
|
|
|
422
|
-
|
|
324
|
+
## Testing
|
|
423
325
|
|
|
424
|
-
|
|
425
|
-
this.editor.commands.unsetHyperlink();
|
|
426
|
-
````
|
|
326
|
+
Cypress runs against the built `dist/` output in a tiny `Bun.serve` playground — the same bytes an npm consumer installs.
|
|
427
327
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
## Get the current value
|
|
434
|
-
|
|
435
|
-
Did you know that you can use `getAttributes` to find out which attributes, for example which href, is currently set? Don’t confuse it with a <u>command</u> (which changes the state), it’s just a method. Here is how that could look like:
|
|
436
|
-
|
|
437
|
-
```js
|
|
438
|
-
this.editor.getAttributes('link').href
|
|
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/
|
|
439
333
|
```
|
|
440
334
|
|
|
441
|
-
|
|
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.
|
|
336
|
+
|
|
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. |
|
|
442
345
|
|
|
443
|
-
|
|
444
|
-
[packages/extension-hyperlink](https://github.com/HMarzban/extension-hyperlink)
|
|
445
|
-
- Extension:
|
|
446
|
-
[packages/extension-hyperlink](https://github.com/HMarzban/extension-hyperlink/tree/main/packages/extension-hyperlink)
|
|
447
|
-
- Usage: [packages/nextjs/src/components/Tiptap.tsx](https://github.com/HMarzban/extension-hyperlink/blob/59f45eba1886202f4840eb2112c34574c16fe68a/packages/nextjs/src/components/Tiptap.tsx#L19-L29)
|
|
346
|
+
The suite also runs from the repo root via `bun run test:all`, alongside the Jest and webapp Cypress suites.
|
|
448
347
|
|
|
449
|
-
##
|
|
348
|
+
## License
|
|
450
349
|
|
|
451
|
-
|
|
350
|
+
MIT
|
|
452
351
|
|
|
453
|
-
|
|
454
|
-
> Please note: We're not affiliated with Tiptap, but we believe in recognizing foundational work.
|
|
352
|
+
## Credits
|
|
455
353
|
|
|
456
|
-
|
|
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).
|