@docs.plus/extension-hyperlink 1.5.2 → 2.1.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.
@@ -1,136 +0,0 @@
1
- import tippy, { Instance, Props, roundArrow } from "tippy.js";
2
- import { Editor } from "@tiptap/core";
3
- import { EditorView } from "@tiptap/pm/view";
4
- import { posToDOMRect } from "@tiptap/core";
5
-
6
- export interface TippyInitOptions {
7
- editor: Editor;
8
- validate?: (url: string) => boolean;
9
- }
10
-
11
- class Tooltip {
12
- private tippyInstance?: Instance;
13
- private preventHide: boolean = false;
14
- private tippyWrapper: HTMLDivElement;
15
- private editor: Editor;
16
- private view: EditorView;
17
-
18
- constructor(options: TippyInitOptions) {
19
- this.editor = options.editor;
20
- this.view = this.editor.view;
21
- this.tippyWrapper = document.createElement("div");
22
- this.tippyWrapper.addEventListener("mousedown", this.mousedownHandler, {
23
- capture: true,
24
- });
25
- this.view.dom.addEventListener("dragstart", this.dragstartHandler);
26
- this.editor.on("blur", this.blurHandler);
27
- }
28
-
29
- init() {
30
- this.tippyWrapper.innerHTML = "";
31
-
32
- return { tippyModal: this.tippyWrapper, tippyInstance: this.tippyInstance };
33
- }
34
-
35
- show() {
36
- this.tippyInstance?.show();
37
- return true;
38
- }
39
-
40
- hide() {
41
- setTimeout(() => this.tippyInstance?.hide());
42
- return false;
43
- }
44
-
45
- private mousedownHandler = () => {
46
- this.preventHide = true;
47
- };
48
-
49
- private dragstartHandler = () => {
50
- this.hide();
51
- };
52
-
53
- private blurHandler = ({ event }: { event: FocusEvent }) => {
54
- if (this.preventHide) {
55
- this.preventHide = false;
56
- return;
57
- }
58
- if (
59
- event?.relatedTarget &&
60
- this.tippyWrapper.parentNode?.contains(event.relatedTarget as Node)
61
- ) {
62
- return;
63
- }
64
- this.hide();
65
- };
66
-
67
- private tippyBlurHandler = (event: FocusEvent) => {
68
- this.blurHandler({ event });
69
- };
70
-
71
- private createTooltip() {
72
- if (!this.editor || !this.editor.options) return;
73
- const { element: editorElement } = this.editor.options;
74
- const editorIsAttached = !!editorElement.parentElement;
75
-
76
- if (this.tippyInstance || !editorIsAttached) {
77
- return;
78
- }
79
-
80
- this.tippyInstance = tippy(editorElement, {
81
- duration: 0,
82
- getReferenceClientRect: null,
83
- content: this.tippyWrapper,
84
- interactive: true,
85
- trigger: "manual",
86
- placement: "bottom",
87
- hideOnClick: true,
88
- onClickOutside: (instance, event) => {
89
- this.hide();
90
- },
91
- onAfterUpdate: (instance, partialProps) => {
92
- this.show();
93
- },
94
- });
95
-
96
- if (this.tippyInstance.popper.firstChild) {
97
- (this.tippyInstance.popper.firstChild as HTMLElement).addEventListener(
98
- "blur",
99
- this.tippyBlurHandler
100
- );
101
- }
102
- }
103
-
104
- update(view: EditorView, option: any = {}) {
105
- this.createTooltip();
106
-
107
- option.arrow = option?.arrow ?? false;
108
-
109
- if (this.tippyInstance) {
110
- this.tippyInstance.setProps({
111
- ...option,
112
- getReferenceClientRect: () => {
113
- const pos = view.state.selection.from;
114
- // width: 0 is a hack to prevent tippy display in the wrong position
115
- return { ...posToDOMRect(view, pos, pos), width: 0 };
116
- },
117
- });
118
- }
119
-
120
- return {};
121
- }
122
-
123
- destroyTooltip() {
124
- if (this.tippyInstance) {
125
- this.tippyInstance.destroy();
126
- this.tippyInstance = undefined;
127
- this.tippyWrapper.removeEventListener("mousedown", this.mousedownHandler, {
128
- capture: true,
129
- });
130
- this.view.dom.removeEventListener("dragstart", this.dragstartHandler);
131
- this.editor.off("blur", this.blurHandler);
132
- }
133
- }
134
- }
135
-
136
- export default Tooltip;
package/src/hyperlink.ts DELETED
@@ -1,277 +0,0 @@
1
- import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
2
- import { Plugin } from "@tiptap/pm/state";
3
- import { find, registerCustomProtocol, reset } from "linkifyjs";
4
- import autoHyperlink from "./helpers/autoHyperlink";
5
- import clickHandler from "./helpers/clickHandler";
6
- import { pasteHandler } from "./helpers/pasteHandler";
7
- import editHyperlinkHelper from "./helpers/editHyperlink";
8
- import Tooltip from "./helpers/tippyHelper";
9
- import { roundArrow } from "tippy.js";
10
- import { TSetHyperlinkModalOptions } from "./modals/setHyperlink";
11
- import { THyperlinkPreviewModalOptions } from "./modals/previewHyperlink";
12
-
13
- export interface LinkProtocolOptions {
14
- scheme: string;
15
- optionalSlashes?: boolean;
16
- }
17
-
18
- export interface HyperlinkOptions {
19
- /**
20
- * If enabled, it adds links as you type.
21
- */
22
- autoHyperlink: boolean;
23
- /**
24
- * An array of custom protocols to be registered with linkifyjs.
25
- */
26
- protocols: Array<LinkProtocolOptions | string>;
27
- /**
28
- * If enabled, links will be opened on click.
29
- */
30
- openOnClick: boolean;
31
- /**
32
- * Adds a link to the current selection if the pasted content only contains an url.
33
- */
34
- hyperlinkOnPaste: boolean;
35
- /**
36
- * A list of HTML attributes to be rendered.
37
- */
38
- HTMLAttributes: Record<string, any>;
39
- /**
40
- * A list of modals to be rendered.
41
- * @default null
42
- * @example
43
- */
44
- modals: {
45
- previewHyperlink?: ((options: THyperlinkPreviewModalOptions) => void | HTMLElement) | null;
46
- setHyperlink?: ((options: TSetHyperlinkModalOptions) => void | HTMLElement) | null;
47
- };
48
- /**
49
- * A validation function that modifies link verification for the auto linker.
50
- * @param url - The url to be validated.
51
- * @returns - True if the url is valid, false otherwise.
52
- */
53
- validate?: (url: string) => boolean;
54
- }
55
-
56
- declare module "@tiptap/core" {
57
- interface Commands<ReturnType> {
58
- link: {
59
- /**
60
- * Edit the hyperlink's text
61
- */
62
- editHyperLinkText: (text: string) => ReturnType;
63
-
64
- /**
65
- * Edit the hyperlink's href value
66
- */
67
- editHyperLinkHref: (href: string) => ReturnType;
68
-
69
- /**
70
- * Edit the hyperlink's
71
- */
72
- editHyperlink: (attributes?: { newText?: string; newURL?: string }) => ReturnType;
73
-
74
- /**
75
- * Set a hyperlink
76
- */
77
- setHyperlink: (attributes?: { href: string; target?: string | null }) => ReturnType;
78
-
79
- /**
80
- * Unset a link mark
81
- */
82
- unsetHyperlink: () => ReturnType;
83
- };
84
- }
85
- }
86
-
87
- export const Hyperlink = Mark.create<HyperlinkOptions>({
88
- name: "hyperlink",
89
-
90
- priority: 1000,
91
-
92
- keepOnSplit: false,
93
-
94
- onCreate() {
95
- this.options.protocols.forEach((protocol) => {
96
- if (typeof protocol === "string") {
97
- registerCustomProtocol(protocol);
98
- return;
99
- }
100
- registerCustomProtocol(protocol.scheme, protocol.optionalSlashes);
101
- });
102
- },
103
-
104
- onDestroy() {
105
- reset();
106
- },
107
-
108
- inclusive() {
109
- return this.options.autoHyperlink;
110
- },
111
-
112
- addOptions() {
113
- return {
114
- openOnClick: true,
115
- hyperlinkOnPaste: true,
116
- autoHyperlink: true,
117
- protocols: [],
118
- HTMLAttributes: {
119
- target: "_blank",
120
- rel: "noopener noreferrer nofollow",
121
- class: null,
122
- },
123
- modals: {
124
- previewHyperlink: null,
125
- setHyperlink: null,
126
- },
127
- validate: undefined,
128
- };
129
- },
130
-
131
- addAttributes() {
132
- return {
133
- href: {
134
- default: null,
135
- },
136
- target: {
137
- default: this.options.HTMLAttributes.target,
138
- },
139
- class: {
140
- default: this.options.HTMLAttributes.class,
141
- },
142
- };
143
- },
144
-
145
- parseHTML() {
146
- return [{ tag: 'a[href]:not([href *= "javascript:" i])' }];
147
- },
148
-
149
- renderHTML({ HTMLAttributes }) {
150
- return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
151
- },
152
-
153
- addCommands() {
154
- return {
155
- setHyperlink:
156
- (attributes) =>
157
- ({ editor, chain }) => {
158
- if (!this.options.modals.setHyperlink) {
159
- return chain()
160
- .setMark(this.name, attributes)
161
- .setMeta("preventAutohyperlink", true)
162
- .run();
163
- } else {
164
- this.options.modals.setHyperlink({
165
- editor,
166
- validate: this.options.validate,
167
- extentionName: this.name,
168
- attributes,
169
- Tooltip,
170
- roundArrow,
171
- });
172
- return true;
173
- }
174
- },
175
-
176
- editHyperlink:
177
- (attributes) =>
178
- ({ editor }) => {
179
- return editHyperlinkHelper({
180
- ...attributes,
181
- editor,
182
- validate: this.options.validate,
183
- });
184
- },
185
-
186
- editHyperLinkText:
187
- (newText) =>
188
- ({ editor }) => {
189
- return editHyperlinkHelper({
190
- editor,
191
- newText,
192
- validate: this.options.validate,
193
- });
194
- },
195
-
196
- editHyperLinkHref:
197
- (newURL) =>
198
- ({ editor }) => {
199
- return editHyperlinkHelper({
200
- editor,
201
- validate: this.options.validate,
202
- newURL,
203
- });
204
- },
205
-
206
- unsetHyperlink:
207
- () =>
208
- ({ chain }) => {
209
- return chain()
210
- .unsetMark(this.name, { extendEmptyMarkRange: true })
211
- .setMeta("preventAutohyperlink", true)
212
- .run();
213
- },
214
- };
215
- },
216
-
217
- addPasteRules() {
218
- return [
219
- markPasteRule({
220
- find: (text) =>
221
- find(text)
222
- .filter((link) => {
223
- if (this.options.validate) {
224
- return this.options.validate(link.value);
225
- }
226
-
227
- return true;
228
- })
229
- .filter((link) => link.isLink)
230
- .map((link) => ({
231
- text: link.value,
232
- index: link.start,
233
- data: link,
234
- })),
235
- type: this.type,
236
- getAttributes: (match) => ({
237
- href: match.data?.href,
238
- }),
239
- }),
240
- ];
241
- },
242
-
243
- addProseMirrorPlugins() {
244
- const plugins: Plugin[] = [];
245
-
246
- if (this.options.autoHyperlink) {
247
- plugins.push(
248
- autoHyperlink({
249
- type: this.type,
250
- validate: this.options.validate,
251
- })
252
- );
253
- }
254
-
255
- if (this.options.openOnClick) {
256
- plugins.push(
257
- clickHandler({
258
- type: this.type,
259
- editor: this.editor,
260
- validate: this.options.validate,
261
- modal: this.options.modals.previewHyperlink,
262
- })
263
- );
264
- }
265
-
266
- if (this.options.hyperlinkOnPaste) {
267
- plugins.push(
268
- pasteHandler({
269
- editor: this.editor,
270
- type: this.type,
271
- })
272
- );
273
- }
274
-
275
- return plugins;
276
- },
277
- });
package/src/index.ts DELETED
@@ -1,17 +0,0 @@
1
- export * from "./hyperlink";
2
-
3
- export * from "./helpers/tippyHelper";
4
-
5
- export * from "./modals/previewHyperlink";
6
-
7
- export * from "./modals/setHyperlink";
8
-
9
- import { previewHyperlinkModal } from "./modals/previewHyperlink";
10
-
11
- import { setHyperlinkModal } from "./modals/setHyperlink";
12
-
13
- import Tooltip from "./helpers/tippyHelper";
14
-
15
- import { Hyperlink } from "./hyperlink";
16
-
17
- export { Hyperlink, Tooltip, previewHyperlinkModal, setHyperlinkModal };
@@ -1,99 +0,0 @@
1
- import { Editor } from "@tiptap/core";
2
- import { roundArrow } from "tippy.js";
3
- import { find } from "linkifyjs";
4
- import Tooltip from "../helpers/tippyHelper";
5
-
6
- type EditHyperlinkModalOptions = {
7
- editor: Editor;
8
- validate?: (url: string) => boolean;
9
- link: HTMLAnchorElement;
10
- hyperlinkModal: HTMLElement;
11
- tippy: Tooltip;
12
- };
13
-
14
- export const editHyperlinkHandler = (options: EditHyperlinkModalOptions) => {
15
- // Create the form
16
- const form = document.createElement("form");
17
-
18
- const buttonsWrapper = document.createElement("div");
19
- const inputsWrapper = document.createElement("div");
20
-
21
- const linkTextInput = document.createElement("input");
22
- const hrefInput = document.createElement("input");
23
- const applyButton = document.createElement("button");
24
-
25
- buttonsWrapper.classList.add("hyperlink-edit-modal__buttons-wrapper");
26
- inputsWrapper.classList.add("hyperlink-edit-modal__inputs-wrapper");
27
-
28
- linkTextInput.type = "text";
29
- linkTextInput.value = options.link?.innerText || "";
30
- linkTextInput.placeholder = "Enter link text";
31
-
32
- hrefInput.type = "text";
33
- hrefInput.value = options.link.href;
34
- hrefInput.placeholder = "Enter href";
35
-
36
- applyButton.type = "submit";
37
- applyButton.classList.add("hyperlink-edit-modal__apply-button");
38
- applyButton.innerText = "Apply";
39
-
40
- buttonsWrapper.append(applyButton);
41
- inputsWrapper.append(linkTextInput, hrefInput);
42
-
43
- // Append to form
44
- form.append(inputsWrapper, buttonsWrapper);
45
-
46
- options.hyperlinkModal.innerHTML = "";
47
- options.hyperlinkModal.appendChild(form);
48
-
49
- hrefInput.addEventListener("keydown", () => {
50
- hrefInput.style.outlineColor = " #dadce0";
51
- });
52
-
53
- hrefInput.addEventListener("keydown", () => {
54
- linkTextInput.style.outlineColor = " #dadce0";
55
- });
56
-
57
- // Handle form submission
58
- form.addEventListener("submit", (e) => {
59
- e.preventDefault();
60
-
61
- const newLinkText = linkTextInput.value;
62
- const newHref = hrefInput.value;
63
-
64
- if (!newHref || !newLinkText) {
65
- if (!newHref) hrefInput.style.outlineColor = "red";
66
- if (!newLinkText) linkTextInput.style.outlineColor = "red";
67
- return;
68
- }
69
-
70
- const sanitizeURL = find(newHref)
71
- .filter((link) => link.isLink)
72
- .filter((link) => {
73
- if (options.validate) {
74
- return options.validate(link.value);
75
- }
76
- return true;
77
- })
78
- .at(0);
79
-
80
- if (!sanitizeURL?.href) {
81
- hrefInput.style.outlineColor = "red";
82
- return;
83
- }
84
-
85
- options.editor.chain().focus().extendMarkRange("hyperlink").editHyperlink({
86
- newURL: newHref,
87
- newText: newLinkText,
88
- });
89
-
90
- options.tippy.hide();
91
- });
92
-
93
- // Show tooltip
94
- options.tippy.update(options.editor.view, {
95
- arrow: roundArrow,
96
- });
97
-
98
- setTimeout(() => linkTextInput.focus());
99
- };
@@ -1,46 +0,0 @@
1
- interface IconProps {
2
- size?: number;
3
- fill?: string;
4
- }
5
-
6
- // prettier-ignore
7
- export const Copy = ({ size = 24, fill = 'rgb(104, 81, 255)' }: IconProps = {}) => {
8
- return `
9
- <svg fill=${fill} viewBox="0 0 24 24" width=${size} xmlns="http://www.w3.org/2000/svg">
10
- <path d="m13 20a5.006 5.006 0 0 0 5-5v-8.757a3.972 3.972 0 0 0 -1.172-2.829l-2.242-2.242a3.972 3.972 0 0 0 -2.829-1.172h-4.757a5.006 5.006 0 0 0 -5 5v10a5.006 5.006 0 0 0 5 5zm-9-5v-10a3 3 0 0 1 3-3s4.919.014 5 .024v1.976a2 2 0 0 0 2 2h1.976c.01.081.024 9 .024 9a3 3 0 0 1 -3 3h-6a3 3 0 0 1 -3-3zm18-7v11a5.006 5.006 0 0 1 -5 5h-9a1 1 0 0 1 0-2h9a3 3 0 0 0 3-3v-11a1 1 0 0 1 2 0z"/>
11
- </svg>
12
- `
13
- }
14
-
15
- export const LinkSlash = ({
16
- size = 24,
17
- fill = "rgb(104, 81, 255)",
18
- }: IconProps = {}) => {
19
- return `
20
- <svg fill=${fill} viewBox="0 0 24 24" width=${size} xmlns="http://www.w3.org/2000/svg">
21
- <path d="M11.953,17.993c-1.557-.074-3.021-.722-4.121-1.823-.23-.23-.446-.482-.641-.748-.325-.446-.227-1.072,.22-1.397,.446-.325,1.071-.227,1.397,.219,.129,.178,.274,.349,.437,.511,.749,.75,1.744,1.19,2.802,1.24,.552,.026,.979,.495,.952,1.046-.025,.536-.468,.953-.998,.953h-.048ZM1.831,13.331C.65,14.511,0,16.081,0,17.75c0,1.669,.65,3.239,1.831,4.419,1.18,1.181,2.749,1.831,4.419,1.831s3.239-.65,4.419-1.831l1.058-1.058c.391-.391,.391-1.023,0-1.414s-1.023-.391-1.414,0l-1.058,1.058c-.803,.803-1.87,1.245-3.005,1.245s-2.202-.442-3.005-1.245-1.245-1.87-1.245-3.005,.442-2.203,1.245-3.005l3.037-3.038c.391-.391,.391-1.024,0-1.414-.391-.391-1.023-.391-1.414,0l-3.037,3.038ZM.293,1.707L22.293,23.707c.195,.195,.451,.293,.707,.293s.512-.098,.707-.293c.391-.391,.391-1.023,0-1.414l-6.581-6.581,5.043-5.043c1.181-1.18,1.831-2.75,1.831-4.419,0-1.669-.65-3.239-1.831-4.419C20.989,.65,19.42,0,17.75,0s-3.239,.65-4.419,1.831l-1.058,1.058c-.391,.391-.391,1.023,0,1.414s1.023,.391,1.414,0l1.058-1.058c.803-.803,1.87-1.245,3.005-1.245s2.202,.442,3.005,1.245,1.245,1.87,1.245,3.005-.442,2.203-1.245,3.005l-5.043,5.043-5.852-5.852c1.612-.808,3.575-.52,4.894,.798,.162,.162,.308,.333,.437,.511,.196,.269,.501,.411,.81,.411,.204,0,.41-.063,.588-.192,.446-.325,.545-.951,.22-1.397-.194-.266-.41-.518-.641-.748-2.095-2.095-5.297-2.408-7.76-.835L1.707,.293c-.391-.391-1.023-.391-1.414,0s-.391,1.023,0,1.414Z"/>
22
- </svg>
23
- `;
24
- };
25
-
26
- export const Pencil = ({
27
- size = 24,
28
- fill = "rgb(104, 81, 255)",
29
- }: IconProps = {}) => {
30
- return `
31
- <svg fill=${fill} viewBox="0 0 24 24" width=${size} xmlns="http://www.w3.org/2000/svg">
32
- <path d="M22.853,1.148a3.626,3.626,0,0,0-5.124,0L1.465,17.412A4.968,4.968,0,0,0,0,20.947V23a1,1,0,0,0,1,1H3.053a4.966,4.966,0,0,0,3.535-1.464L22.853,6.271A3.626,3.626,0,0,0,22.853,1.148ZM5.174,21.122A3.022,3.022,0,0,1,3.053,22H2V20.947a2.98,2.98,0,0,1,.879-2.121L15.222,6.483l2.3,2.3ZM21.438,4.857,18.932,7.364l-2.3-2.295,2.507-2.507a1.623,1.623,0,1,1,2.295,2.3Z"/>
33
- </svg>
34
- `;
35
- };
36
-
37
- export const AngleUp = ({
38
- size = 32,
39
- fill = "rgb(104, 81, 255)",
40
- }: IconProps = {}) => {
41
- return `
42
- <svg fill=${fill} viewBox="0 0 24 24" width=${size} xmlns="http://www.w3.org/2000/svg">
43
- <path d="M17.293,15.207,12,9.914,6.707,15.207,5.293,13.793,10.586,8.5a2,2,0,0,1,2.828,0l5.293,5.293Z"/>
44
- </svg>
45
- `;
46
- };
@@ -1,85 +0,0 @@
1
- import { Editor } from "@tiptap/core";
2
- import { editHyperlinkHandler } from "./editeHyperlink";
3
- import { Copy, LinkSlash, Pencil } from "./icons";
4
- import Tooltip from "../helpers/tippyHelper";
5
-
6
- export type THyperlinkPreviewModalOptions = {
7
- editor: Editor;
8
- validate?: (url: string) => boolean;
9
- link: HTMLAnchorElement;
10
- node?: any;
11
- nodePos: number;
12
- tippy: Tooltip;
13
- };
14
-
15
- export function previewHyperlinkModal(options: THyperlinkPreviewModalOptions): HTMLElement {
16
- const href = options.link.href;
17
-
18
- const hyperlinkModal = document.createElement("div");
19
- const removeButton = document.createElement("button");
20
- const copyButton = document.createElement("button");
21
- const editButton = document.createElement("button");
22
-
23
- const newBubble = document.createElement("div");
24
- newBubble.classList.add("hyperlink-preview-modal__metadata");
25
-
26
- const hrefTitle = document.createElement("a");
27
- hrefTitle.setAttribute("target", "_blank");
28
- hrefTitle.setAttribute("rel", "noreferrer");
29
- hrefTitle.setAttribute("href", href);
30
- hrefTitle.innerText = href;
31
-
32
- newBubble.append(hrefTitle);
33
-
34
- fetch("/api/metadata", {
35
- method: "POST",
36
- headers: { "Content-Type": "application/json" },
37
- body: JSON.stringify({ url: href }),
38
- })
39
- .then((response) => response.json())
40
- .then((data) => {
41
- // Create a new bubble with the title
42
- hrefTitle.setAttribute("href", href);
43
-
44
- hrefTitle.innerText = data.title || data["og:title"] || href;
45
- newBubble.replaceChildren(hrefTitle);
46
-
47
- // Create an image element if image exists in metadata
48
- if (data.icon || data.image || data["og:image"]) {
49
- const img = document.createElement("img");
50
- img.src = data.icon || data.image || data["og:image"];
51
- img.alt = data.title || data["og:title"] || "hyperlink image";
52
- newBubble.appendChild(img);
53
- }
54
- })
55
- .catch((error) => {
56
- console.error("Error fetching metadata:", error);
57
- });
58
-
59
- hyperlinkModal.classList.add("hyperlink-preview-modal");
60
-
61
- removeButton.classList.add("hyperlink-preview-modal__remove-button");
62
- removeButton.innerHTML = LinkSlash();
63
-
64
- editButton.classList.add("hyperlink-preview-modal__edit-button");
65
- editButton.innerHTML = Pencil();
66
-
67
- copyButton.classList.add("hyperlink-preview-modal__copy-button");
68
- copyButton.innerHTML = Copy();
69
-
70
- removeButton.addEventListener("click", () => {
71
- options.tippy.hide();
72
- return options.editor.chain().focus().unsetHyperlink().run();
73
- });
74
-
75
- editButton.addEventListener("click", () => editHyperlinkHandler({ ...options, hyperlinkModal }));
76
-
77
- copyButton.addEventListener("click", () => {
78
- options.tippy.hide();
79
- navigator.clipboard.writeText(href);
80
- });
81
-
82
- hyperlinkModal.append(newBubble, copyButton, editButton, removeButton);
83
-
84
- return hyperlinkModal;
85
- }