@limetech/lime-elements 38.28.0 → 38.28.1
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 +12 -0
- package/README.md +10 -1
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +64 -27
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js.map +1 -1
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/link/link-plugin.js +64 -27
- package/dist/collection/components/text-editor/prosemirror-adapter/plugins/link/link-plugin.js.map +1 -1
- package/dist/esm/limel-prosemirror-adapter.entry.js +64 -27
- package/dist/esm/limel-prosemirror-adapter.entry.js.map +1 -1
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/{p-202107e9.entry.js → p-6c60172e.entry.js} +2 -2
- package/dist/lime-elements/p-6c60172e.entry.js.map +1 -0
- package/package.json +1 -1
- package/dist/lime-elements/p-202107e9.entry.js.map +0 -1
package/dist/collection/components/text-editor/prosemirror-adapter/plugins/link/link-plugin.js
CHANGED
|
@@ -86,9 +86,14 @@ const getLinkDataAtPosition = (view, event) => {
|
|
|
86
86
|
return { href: href, text: text, from: from, to: to };
|
|
87
87
|
};
|
|
88
88
|
const processModClickEvent = (view, event) => {
|
|
89
|
-
const
|
|
89
|
+
const linkData = getLinkDataAtPosition(view, event);
|
|
90
|
+
if (!linkData.href) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
const { href } = linkData;
|
|
90
95
|
if (href) {
|
|
91
|
-
window.open(href, '_blank');
|
|
96
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
92
97
|
return true;
|
|
93
98
|
}
|
|
94
99
|
return false;
|
|
@@ -101,32 +106,21 @@ const openLinkMenu = (view, href, text) => {
|
|
|
101
106
|
});
|
|
102
107
|
view.dom.dispatchEvent(event);
|
|
103
108
|
};
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const processClickEvent = (view, event) => {
|
|
108
|
-
const now = Date.now();
|
|
109
|
-
if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
|
|
110
|
-
clearTimeout(clickTimeout);
|
|
111
|
-
lastClickTime = now; // Reset lastClickTime to prevent single-click action
|
|
109
|
+
const processDoubleClickEvent = (view, event) => {
|
|
110
|
+
const linkData = getLinkDataAtPosition(view, event);
|
|
111
|
+
if (!linkData) {
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const { href, text, from, to } = linkData;
|
|
119
|
-
const transaction = view.state.tr.setSelection(TextSelection.create(view.state.doc, from, to));
|
|
120
|
-
view.dispatch(transaction);
|
|
121
|
-
openLinkMenu(view, href, text);
|
|
122
|
-
}
|
|
123
|
-
}, DOUBLE_CLICK_DELAY);
|
|
114
|
+
const { href, text, from, to } = linkData;
|
|
115
|
+
const transaction = view.state.tr.setSelection(TextSelection.create(view.state.doc, from, to));
|
|
116
|
+
view.dispatch(transaction);
|
|
117
|
+
openLinkMenu(view, href, text);
|
|
124
118
|
return true;
|
|
125
119
|
};
|
|
126
120
|
/**
|
|
127
|
-
* Regular expression for matching URLs, mailto links, and
|
|
121
|
+
* Regular expression for matching URLs, mailto links, phone links, and bare www-links
|
|
128
122
|
*/
|
|
129
|
-
const URL_REGEX = /(https?:\/\/[^\s<>"']+|mailto:[^\s<>"']+|tel:[^\s<>"']+)/g;
|
|
123
|
+
const URL_REGEX = /(https?:\/\/[^\s<>"']+|mailto:[^\s<>"']+|tel:[^\s<>"']+|www\.[^\s<>"']+)/g;
|
|
130
124
|
/**
|
|
131
125
|
* Checks if the text contains any URLs, mailto links, or phone links
|
|
132
126
|
* @param text
|
|
@@ -150,8 +144,19 @@ const createTextNode = (schema, content) => {
|
|
|
150
144
|
* @param url
|
|
151
145
|
*/
|
|
152
146
|
const createLinkNode = (schema, url) => {
|
|
153
|
-
const
|
|
154
|
-
|
|
147
|
+
const normalizeUrlForLinkMark = (input) => {
|
|
148
|
+
let output = input.trim();
|
|
149
|
+
while (output.endsWith('\\')) {
|
|
150
|
+
output = output.slice(0, -1);
|
|
151
|
+
}
|
|
152
|
+
if (output.toLowerCase().startsWith('www.')) {
|
|
153
|
+
output = `https://${output}`;
|
|
154
|
+
}
|
|
155
|
+
return output;
|
|
156
|
+
};
|
|
157
|
+
const normalizedUrl = normalizeUrlForLinkMark(url);
|
|
158
|
+
const linkMark = schema.marks.link.create(getLinkAttributes(normalizedUrl, normalizedUrl));
|
|
159
|
+
return schema.text(normalizedUrl, [linkMark]);
|
|
155
160
|
};
|
|
156
161
|
/**
|
|
157
162
|
* Finds all link matches in the provided text
|
|
@@ -171,6 +176,36 @@ const findLinkMatches = (text) => {
|
|
|
171
176
|
}
|
|
172
177
|
return matches;
|
|
173
178
|
};
|
|
179
|
+
/**
|
|
180
|
+
* Creates nodes for the pasted text while preserving soft line breaks.
|
|
181
|
+
* - Each newline becomes a `hard_break`.
|
|
182
|
+
* - Empty lines are preserved (consecutive newlines => multiple `hard_break`s).
|
|
183
|
+
* - URLs inside each line are converted to link-marked text.
|
|
184
|
+
* @param text - Raw pasted text
|
|
185
|
+
* @param schema - ProseMirror schema
|
|
186
|
+
*/
|
|
187
|
+
const createNodesWithLinksAndBreaks = (text, schema) => {
|
|
188
|
+
// Split preserves empty lines between consecutive newlines
|
|
189
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
190
|
+
const nodes = [];
|
|
191
|
+
for (const [index, line] of lines.entries()) {
|
|
192
|
+
if (line.length > 0) {
|
|
193
|
+
nodes.push(...createNodesWithLinks(line, schema));
|
|
194
|
+
}
|
|
195
|
+
if (index < lines.length - 1) {
|
|
196
|
+
const hb = schema.nodes.hard_break;
|
|
197
|
+
if (hb) {
|
|
198
|
+
nodes.push(hb.create());
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Fallback: if schema lacks hard_break, defer to default paste behavior
|
|
202
|
+
// (Do NOT throw; keep behavior stable across versions)
|
|
203
|
+
console.warn('hard_break node not found in schema');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return nodes;
|
|
208
|
+
};
|
|
174
209
|
/**
|
|
175
210
|
* Creates text nodes with links for any URLs, mailto links, or phone links found in the text
|
|
176
211
|
* @param text
|
|
@@ -272,7 +307,8 @@ const processPasteEvent = (view, event) => {
|
|
|
272
307
|
if (!text || !hasUrls(text)) {
|
|
273
308
|
return false;
|
|
274
309
|
}
|
|
275
|
-
const nodes =
|
|
310
|
+
const nodes = createNodesWithLinksAndBreaks(text, view.state.schema);
|
|
311
|
+
event.preventDefault();
|
|
276
312
|
pasteAsLink(view, nodes);
|
|
277
313
|
return true;
|
|
278
314
|
};
|
|
@@ -289,11 +325,12 @@ export const createLinkPlugin = (updateLinkCallback) => {
|
|
|
289
325
|
event.button === 0) {
|
|
290
326
|
return processModClickEvent(view, event);
|
|
291
327
|
}
|
|
328
|
+
},
|
|
329
|
+
dblclick: (view, event) => {
|
|
292
330
|
if (event.button !== MouseButtons.Right) {
|
|
293
331
|
// We want to ignore right-clicks
|
|
294
|
-
return
|
|
332
|
+
return processDoubleClickEvent(view, event);
|
|
295
333
|
}
|
|
296
|
-
return true;
|
|
297
334
|
},
|
|
298
335
|
click: (_view, event) => {
|
|
299
336
|
if (!(event.target instanceof HTMLElement)) {
|
package/dist/collection/components/text-editor/prosemirror-adapter/plugins/link/link-plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link-plugin.js","sourceRoot":"","sources":["../../../../../../src/components/text-editor/prosemirror-adapter/plugins/link/link-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAErE,OAAO,EAAQ,QAAQ,EAAgB,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,SAAS,CAAC,YAAY,CAAC,CAAC;AAIzD,MAAM,UAAU,GAAG,CACf,IAAgB,EAChB,kBAAuC,EACzC,EAAE;EACA,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;EAE1C,IAAI,IAAI,GAAG,EAAE,CAAC;EACd,IAAI,IAAI,GAAG,EAAE,CAAC;EACd,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAChD,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE;MAC3B,OAAO;KACV;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,GAAG,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,GAAG,GAAG,CAAC,CAAC;IAEtD,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAE9C,qDAAqD;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAU,EAAE,EAAE;MAC9B,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE;QAC3B,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;OAC1B;IACL,CAAC,CAAC,CAAC;EACP,CAAC,CAAC,CAAC;EAEH,IAAI,kBAAkB,EAAE;IACpB,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;GAClC;AACL,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;EACjC,OAAO,GAAG,GAAG,CAAC,EAAE;IACZ,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACjC,IACI,CAAC,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,MAAM,CAAA;MACb,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CACZ,CAAC,IAAU,EAAE,EAAE,CACX,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,IAAI;QACvC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAC/B,EACH;MACE,MAAM;KACT;IAED,GAAG,EAAE,CAAC;GACT;EAED,OAAO,GAAG,CAAC;AACf,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;EAC/B,OAAO,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE;IAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7B,IACI,CAAC,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,MAAM,CAAA;MACb,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CACZ,CAAC,IAAI,EAAE,EAAE,CACL,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,IAAI;QACvC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAC/B,EACH;MACE,MAAM;KACT;IAED,GAAG,EAAE,CAAC;GACT;EAED,OAAO,GAAG,CAAC;AACf,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,qBAAqB,GAAG,CAAC,IAAgB,EAAE,KAAiB,EAAE,EAAE;EAClE,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;EAC1E,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,GAAG,CAAC,CAAC;EAC7C,IAAI,CAAC,IAAI,EAAE;IACP,OAAO,IAAI,CAAC;GACf;EAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAC5B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe,CAAC,IAAI,CACpD,CAAC;EACF,IAAI,CAAC,QAAQ,EAAE;IACX,OAAO,IAAI,CAAC;GACf;EAED,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;EACjC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;EACtD,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;EAClD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;EAEvD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,oBAAoB,GAAG,CAAC,IAAgB,EAAE,KAAiB,EAAW,EAAE;EAC1E,MAAM,EAAE,IAAI,EAAE,GAAG,qBAAqB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;EACpD,IAAI,IAAI,EAAE;IACN,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAE5B,OAAO,IAAI,CAAC;GACf;EAED,OAAO,KAAK,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,IAAgB,EAAE,IAAY,EAAE,IAAY,EAAE,EAAE;EAClE,MAAM,KAAK,GAAG,IAAI,WAAW,CAAa,uBAAuB,EAAE;IAC/D,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAClC,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;GACjB,CAAC,CAAC;EACH,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC,CAAC;AAEF,IAAI,aAAa,GAAG,CAAC,CAAC;AACtB,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,IAAI,YAAY,CAAC;AAEjB,MAAM,iBAAiB,GAAG,CAAC,IAAgB,EAAE,KAAiB,EAAW,EAAE;EACvE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;EAEvB,IAAI,GAAG,GAAG,aAAa,GAAG,kBAAkB,EAAE;IAC1C,YAAY,CAAC,YAAY,CAAC,CAAC;IAC3B,aAAa,GAAG,GAAG,CAAC,CAAC,qDAAqD;IAE1E,OAAO,KAAK,CAAC;GAChB;EAED,aAAa,GAAG,GAAG,CAAC;EAEpB,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;IAC3B,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACpD,IAAI,QAAQ,EAAE;MACV,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,QAAQ,CAAC;MAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAC1C,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CACjD,CAAC;MACF,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;MAC3B,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;KAClC;EACL,CAAC,EAAE,kBAAkB,CAAC,CAAC;EAEvB,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,SAAS,GAAG,2DAA2D,CAAC;AAE9E;;;GAGG;AACH,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE;EACtC,yBAAyB;EACzB,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC;EAExB,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,cAAc,GAAG,CAAC,MAAc,EAAE,OAAe,EAAQ,EAAE;EAC7D,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,cAAc,GAAG,CAAC,MAAc,EAAE,GAAW,EAAQ,EAAE;EACzD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;EAEvE,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACxC,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,eAAe,GAAG,CACpB,IAAY,EACsC,EAAE;EACpD,MAAM,OAAO,GAAG,EAAE,CAAC;EACnB,IAAI,KAA6B,CAAC;EAElC,yBAAyB;EACzB,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC;EAExB,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE;IAC5C,OAAO,CAAC,IAAI,CAAC;MACT,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;MACb,KAAK,EAAE,KAAK,CAAC,KAAK;MAClB,GAAG,EAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM;KACrC,CAAC,CAAC;GACN;EAED,OAAO,OAAO,CAAC;AACnB,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,CAAC,IAAY,EAAE,MAAc,EAAU,EAAE;EAClE,MAAM,KAAK,GAAW,EAAE,CAAC;EACzB,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;EAEtC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE;IACtB,wDAAwD;IACxD,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;GACzC;EAED,IAAI,SAAS,GAAG,CAAC,CAAC;EAElB,qBAAqB;EACrB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE;IACzB,0CAA0C;IAC1C,IAAI,KAAK,CAAC,KAAK,GAAG,SAAS,EAAE;MACzB,KAAK,CAAC,IAAI,CACN,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAC7D,CAAC;KACL;IAED,oBAAoB;IACpB,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9C,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC;GACzB;EAED,6CAA6C;EAC7C,IAAI,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE;IACzB,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;GAC7D;EAED,OAAO,KAAK,CAAC;AACjB,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,GAAG,CAAC,IAAgB,EAAE,KAAa,EAAE,EAAE;EACpD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;IACpB,OAAO;GACV;EAED,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE;IACzB,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;GACpC;OAAM;IACH,kBAAkB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;GACnC;AACL,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,gBAAgB,GAAG,CAAC,KAAa,EAAW,EAAE;EAChD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;IACpB,OAAO,KAAK,CAAC;GAChB;EAED,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;EAEtB,sCAAsC;EACtC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;IACvD,OAAO,KAAK,CAAC;GAChB;EAED,0FAA0F;EAC1F,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,CAAC,IAAgB,EAAE,QAAc,EAAE,EAAE;EAC1D,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;EACjC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC;EAErC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;EAE1E,kEAAkE;EAClE,MAAM,YAAY,GACd,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;EAEhE,wCAAwC;EACxC,QAAQ,CACJ,KAAK,CAAC,EAAE;KACH,UAAU,CAAC,YAAY,EAAE,IAAI,EAAE,EAAE,CAAC;KAClC,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAC3D,CAAC;AACN,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,CAAC,IAAgB,EAAE,KAAa,EAAE,EAAE;EAC3D,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;EACjC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC;EAErC,4CAA4C;EAC5C,MAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;EAE3C,kDAAkD;EAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;AACvD,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,CACtB,IAAgB,EAChB,KAAqB,EACd,EAAE;;EACT,MAAM,IAAI,GAAG,MAAA,KAAK,CAAC,aAAa,0CAAE,OAAO,CAAC,YAAY,CAAC,CAAC;EACxD,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;IACzB,OAAO,KAAK,CAAC;GAChB;EAED,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;EAC5D,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;EAEzB,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,kBAAuC,EAAE,EAAE;EACxE,OAAO,IAAI,MAAM,CAAC;IACd,GAAG,EAAE,aAAa;IAClB,KAAK,EAAE;MACH,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACzB,OAAO,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;MAC1C,CAAC;MACD,eAAe,EAAE;QACb,SAAS,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;UACvB,IACI,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC;YAChC,KAAK,CAAC,MAAM,KAAK,CAAC,EACpB;YACE,OAAO,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;WAC5C;UAED,IAAI,KAAK,CAAC,MAAM,KAAK,YAAY,CAAC,KAAK,EAAE;YACrC,iCAAiC;YACjC,OAAO,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;WACzC;UAED,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;UACpB,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY,WAAW,CAAC,EAAE;YACxC,OAAO;WACV;UAED,4DAA4D;UAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;UACvC,IAAI,IAAI,EAAE;YACN,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,eAAe,EAAE,CAAC;WAC3B;QACL,CAAC;OACJ;KACJ;IACD,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;MACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QACb,UAAU,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;MACzC,CAAC;KACJ,CAAC;GACL,CAAC,CAAC;AACP,CAAC,CAAC","sourcesContent":["import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { Mark, Fragment, Node, Schema } from 'prosemirror-model';\nimport { EditorMenuTypes, MouseButtons } from '../../menu/types';\nimport { EditorLink } from '../../../text-editor.types';\nimport { getLinkAttributes } from './utils';\n\nexport const linkPluginKey = new PluginKey('linkPlugin');\n\nexport type UpdateLinkCallback = (text: string, href: string) => void;\n\nconst updateLink = (\n view: EditorView,\n updateLinkCallback?: UpdateLinkCallback\n) => {\n const { from, to } = view.state.selection;\n\n let text = '';\n let href = '';\n view.state.doc.nodesBetween(from, to, (node, pos) => {\n if (node.type.name !== 'text') {\n return;\n }\n\n const fromInNode = Math.max(0, from - pos);\n const toInNode = Math.min(node.text.length, to - pos);\n\n text += node.text.slice(fromInNode, toInNode);\n\n // eslint-disable-next-line unicorn/no-array-for-each\n node.marks.forEach((mark: Mark) => {\n if (mark.type.name === 'link') {\n href = mark.attrs.href;\n }\n });\n });\n\n if (updateLinkCallback) {\n updateLinkCallback(text, href);\n }\n};\n\n/**\n * Finds the start position of the link node ensuring the href matches the original link's href.\n * @param doc - The ProseMirror document.\n * @param pos - The position to start searching from.\n * @param href - The href attribute of the original link mark.\n * @returns The start position of the link node.\n */\nconst findStart = (doc, pos, href) => {\n while (pos > 0) {\n const node = doc.nodeAt(pos - 1);\n if (\n !node?.isText ||\n !node.marks.some(\n (mark: Mark) =>\n mark.type.name === EditorMenuTypes.Link &&\n mark.attrs.href === href\n )\n ) {\n break;\n }\n\n pos--;\n }\n\n return pos;\n};\n\n/**\n * Finds the end position of the link node ensuring the href matches the original link's href.\n * @param doc - The ProseMirror document.\n * @param pos - The position to start searching from.\n * @param href - The href attribute of the original link mark.\n * @returns The end position of the link node.\n */\nconst findEnd = (doc, pos, href) => {\n while (pos < doc.content.size) {\n const node = doc.nodeAt(pos);\n if (\n !node?.isText ||\n !node.marks.some(\n (mark) =>\n mark.type.name === EditorMenuTypes.Link &&\n mark.attrs.href === href\n )\n ) {\n break;\n }\n\n pos++;\n }\n\n return pos;\n};\n\n/**\n * Gets the link data at the specified position.\n * @param view - The ProseMirror editor view.\n * @param event - The mouse event.\n * @returns An object containing the link data or null if no link is found.\n */\nconst getLinkDataAtPosition = (view: EditorView, event: MouseEvent) => {\n const pos = view.posAtCoords({ left: event.clientX, top: event.clientY });\n const node = view.state.doc.nodeAt(pos?.pos);\n if (!node) {\n return null;\n }\n\n const linkMark = node.marks.find(\n (mark) => mark.type.name === EditorMenuTypes.Link\n );\n if (!linkMark) {\n return null;\n }\n\n const href = linkMark.attrs.href;\n const from = findStart(view.state.doc, pos.pos, href);\n const to = findEnd(view.state.doc, pos.pos, href);\n const text = view.state.doc.textBetween(from, to, ' ');\n\n return { href: href, text: text, from: from, to: to };\n};\n\nconst processModClickEvent = (view: EditorView, event: MouseEvent): boolean => {\n const { href } = getLinkDataAtPosition(view, event);\n if (href) {\n window.open(href, '_blank');\n\n return true;\n }\n\n return false;\n};\n\nconst openLinkMenu = (view: EditorView, href: string, text: string) => {\n const event = new CustomEvent<EditorLink>('open-editor-link-menu', {\n detail: { href: href, text: text },\n bubbles: true,\n composed: true,\n });\n view.dom.dispatchEvent(event);\n};\n\nlet lastClickTime = 0;\nconst DOUBLE_CLICK_DELAY = 200;\nlet clickTimeout;\n\nconst processClickEvent = (view: EditorView, event: MouseEvent): boolean => {\n const now = Date.now();\n\n if (now - lastClickTime < DOUBLE_CLICK_DELAY) {\n clearTimeout(clickTimeout);\n lastClickTime = now; // Reset lastClickTime to prevent single-click action\n\n return false;\n }\n\n lastClickTime = now;\n\n clickTimeout = setTimeout(() => {\n const linkData = getLinkDataAtPosition(view, event);\n if (linkData) {\n const { href, text, from, to } = linkData;\n const transaction = view.state.tr.setSelection(\n TextSelection.create(view.state.doc, from, to)\n );\n view.dispatch(transaction);\n openLinkMenu(view, href, text);\n }\n }, DOUBLE_CLICK_DELAY);\n\n return true;\n};\n\n/**\n * Regular expression for matching URLs, mailto links, and phone links\n */\nconst URL_REGEX = /(https?:\\/\\/[^\\s<>\"']+|mailto:[^\\s<>\"']+|tel:[^\\s<>\"']+)/g;\n\n/**\n * Checks if the text contains any URLs, mailto links, or phone links\n * @param text\n */\nconst hasUrls = (text: string): boolean => {\n // Reset regex before use\n URL_REGEX.lastIndex = 0;\n\n return URL_REGEX.test(text);\n};\n\n/**\n * Creates a text node with the provided content\n * @param schema\n * @param content\n */\nconst createTextNode = (schema: Schema, content: string): Node => {\n return schema.text(content);\n};\n\n/**\n * Creates a link node with the provided URL\n * @param schema\n * @param url\n */\nconst createLinkNode = (schema: Schema, url: string): Node => {\n const linkMark = schema.marks.link.create(getLinkAttributes(url, url));\n\n return schema.text(url, [linkMark]);\n};\n\n/**\n * Finds all link matches in the provided text\n * @param text\n */\nconst findLinkMatches = (\n text: string\n): Array<{ url: string; start: number; end: number }> => {\n const matches = [];\n let match: RegExpExecArray | null;\n\n // Reset regex before use\n URL_REGEX.lastIndex = 0;\n\n while ((match = URL_REGEX.exec(text)) !== null) {\n matches.push({\n url: match[0],\n start: match.index,\n end: match.index + match[0].length,\n });\n }\n\n return matches;\n};\n\n/**\n * Creates text nodes with links for any URLs, mailto links, or phone links found in the text\n * @param text\n * @param schema\n */\nconst createNodesWithLinks = (text: string, schema: Schema): Node[] => {\n const nodes: Node[] = [];\n const matches = findLinkMatches(text);\n\n if (matches.length === 0) {\n // No links found, just return the text as a single node\n return [createTextNode(schema, text)];\n }\n\n let lastIndex = 0;\n\n // Process each match\n for (const match of matches) {\n // Add text before the current link if any\n if (match.start > lastIndex) {\n nodes.push(\n createTextNode(schema, text.slice(lastIndex, match.start))\n );\n }\n\n // Add the link node\n nodes.push(createLinkNode(schema, match.url));\n\n lastIndex = match.end;\n }\n\n // Add any remaining text after the last link\n if (lastIndex < text.length) {\n nodes.push(createTextNode(schema, text.slice(lastIndex)));\n }\n\n return nodes;\n};\n\n/**\n * Pastes nodes at the current selection\n * @param view - The editor view\n * @param nodes - Array of nodes to paste\n */\nconst pasteAsLink = (view: EditorView, nodes: Node[]) => {\n if (nodes.length === 0) {\n return;\n }\n\n if (isSingleLinkNode(nodes)) {\n insertSingleLink(view, nodes[0]);\n } else {\n insertNodeFragment(view, nodes);\n }\n};\n\n/**\n * Checks if the nodes array contains just a single link node\n * @param nodes\n */\nconst isSingleLinkNode = (nodes: Node[]): boolean => {\n if (nodes.length !== 1) {\n return false;\n }\n\n const node = nodes[0];\n\n // Must be text with non-empty content\n if (!node.isText || !node.text || node.text.trim() === '') {\n return false;\n }\n\n // Must have a link mark (even if there are other marks, we just care about link presence)\n return node.marks.some((mark) => mark.type.name === 'link');\n};\n\n/**\n * Inserts a single link node, applying it to selected text if present\n * @param view\n * @param linkNode\n */\nconst insertSingleLink = (view: EditorView, linkNode: Node) => {\n const { state, dispatch } = view;\n const { from, to } = state.selection;\n\n const linkMark = linkNode.marks.find((mark) => mark.type.name === 'link');\n\n // Use selected text if there's a selection, otherwise use the URL\n const selectedText =\n state.doc.textBetween(from, to, ' ') || linkMark.attrs.href;\n\n // Insert the text and add the link mark\n dispatch(\n state.tr\n .insertText(selectedText, from, to)\n .addMark(from, from + selectedText.length, linkMark)\n );\n};\n\n/**\n * Inserts multiple nodes as a fragment at the current selection\n * @param view - The editor view\n * @param nodes - Array of nodes to insert\n */\nconst insertNodeFragment = (view: EditorView, nodes: Node[]) => {\n const { state, dispatch } = view;\n const { from, to } = state.selection;\n\n // Create a fragment from the array of nodes\n const fragment = Fragment.fromArray(nodes);\n\n // Replace the current selection with the fragment\n dispatch(state.tr.replaceWith(from, to, fragment));\n};\n\n/**\n * Handles pasted content, converting URLs to links\n * @param view\n * @param event\n */\nconst processPasteEvent = (\n view: EditorView,\n event: ClipboardEvent\n): boolean => {\n const text = event.clipboardData?.getData('text/plain');\n if (!text || !hasUrls(text)) {\n return false;\n }\n\n const nodes = createNodesWithLinks(text, view.state.schema);\n pasteAsLink(view, nodes);\n\n return true;\n};\n\nexport const createLinkPlugin = (updateLinkCallback?: UpdateLinkCallback) => {\n return new Plugin({\n key: linkPluginKey,\n props: {\n handlePaste: (view, event) => {\n return processPasteEvent(view, event);\n },\n handleDOMEvents: {\n mousedown: (view, event) => {\n if (\n (event.metaKey || event.ctrlKey) &&\n event.button === 0\n ) {\n return processModClickEvent(view, event);\n }\n\n if (event.button !== MouseButtons.Right) {\n // We want to ignore right-clicks\n return processClickEvent(view, event);\n }\n\n return true;\n },\n click: (_view, event) => {\n if (!(event.target instanceof HTMLElement)) {\n return;\n }\n\n // Prevent unhandled navigation and bubbling for link clicks\n const link = event.target.closest('a');\n if (link) {\n event.preventDefault();\n event.stopPropagation();\n }\n },\n },\n },\n view: () => ({\n update: (view) => {\n updateLink(view, updateLinkCallback);\n },\n }),\n });\n};\n"]}
|
|
1
|
+
{"version":3,"file":"link-plugin.js","sourceRoot":"","sources":["../../../../../../src/components/text-editor/prosemirror-adapter/plugins/link/link-plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { Mark, Fragment, Node, Schema } from 'prosemirror-model';\nimport { EditorMenuTypes, MouseButtons } from '../../menu/types';\nimport { EditorLink } from '../../../text-editor.types';\nimport { getLinkAttributes } from './utils';\n\nexport const linkPluginKey = new PluginKey('linkPlugin');\n\nexport type UpdateLinkCallback = (text: string, href: string) => void;\n\nconst updateLink = (\n view: EditorView,\n updateLinkCallback?: UpdateLinkCallback\n) => {\n const { from, to } = view.state.selection;\n\n let text = '';\n let href = '';\n view.state.doc.nodesBetween(from, to, (node, pos) => {\n if (node.type.name !== 'text') {\n return;\n }\n\n const fromInNode = Math.max(0, from - pos);\n const toInNode = Math.min(node.text.length, to - pos);\n\n text += node.text.slice(fromInNode, toInNode);\n\n // eslint-disable-next-line unicorn/no-array-for-each\n node.marks.forEach((mark: Mark) => {\n if (mark.type.name === 'link') {\n href = mark.attrs.href;\n }\n });\n });\n\n if (updateLinkCallback) {\n updateLinkCallback(text, href);\n }\n};\n\n/**\n * Finds the start position of the link node ensuring the href matches the original link's href.\n * @param doc - The ProseMirror document.\n * @param pos - The position to start searching from.\n * @param href - The href attribute of the original link mark.\n * @returns The start position of the link node.\n */\nconst findStart = (doc, pos, href) => {\n while (pos > 0) {\n const node = doc.nodeAt(pos - 1);\n if (\n !node?.isText ||\n !node.marks.some(\n (mark: Mark) =>\n mark.type.name === EditorMenuTypes.Link &&\n mark.attrs.href === href\n )\n ) {\n break;\n }\n\n pos--;\n }\n\n return pos;\n};\n\n/**\n * Finds the end position of the link node ensuring the href matches the original link's href.\n * @param doc - The ProseMirror document.\n * @param pos - The position to start searching from.\n * @param href - The href attribute of the original link mark.\n * @returns The end position of the link node.\n */\nconst findEnd = (doc, pos, href) => {\n while (pos < doc.content.size) {\n const node = doc.nodeAt(pos);\n if (\n !node?.isText ||\n !node.marks.some(\n (mark) =>\n mark.type.name === EditorMenuTypes.Link &&\n mark.attrs.href === href\n )\n ) {\n break;\n }\n\n pos++;\n }\n\n return pos;\n};\n\n/**\n * Gets the link data at the specified position.\n * @param view - The ProseMirror editor view.\n * @param event - The mouse event.\n * @returns An object containing the link data or null if no link is found.\n */\nconst getLinkDataAtPosition = (view: EditorView, event: MouseEvent) => {\n const pos = view.posAtCoords({ left: event.clientX, top: event.clientY });\n const node = view.state.doc.nodeAt(pos?.pos);\n if (!node) {\n return null;\n }\n\n const linkMark = node.marks.find(\n (mark) => mark.type.name === EditorMenuTypes.Link\n );\n if (!linkMark) {\n return null;\n }\n\n const href = linkMark.attrs.href;\n const from = findStart(view.state.doc, pos.pos, href);\n const to = findEnd(view.state.doc, pos.pos, href);\n const text = view.state.doc.textBetween(from, to, ' ');\n\n return { href: href, text: text, from: from, to: to };\n};\n\nconst processModClickEvent = (view: EditorView, event: MouseEvent): boolean => {\n const linkData = getLinkDataAtPosition(view, event);\n if (!linkData.href) {\n return false;\n }\n event.preventDefault();\n\n const { href } = linkData;\n if (href) {\n window.open(href, '_blank', 'noopener,noreferrer');\n\n return true;\n }\n\n return false;\n};\n\nconst openLinkMenu = (view: EditorView, href: string, text: string) => {\n const event = new CustomEvent<EditorLink>('open-editor-link-menu', {\n detail: { href: href, text: text },\n bubbles: true,\n composed: true,\n });\n view.dom.dispatchEvent(event);\n};\n\nconst processDoubleClickEvent = (\n view: EditorView,\n event: MouseEvent\n): boolean => {\n const linkData = getLinkDataAtPosition(view, event);\n if (!linkData) {\n return false;\n }\n\n const { href, text, from, to } = linkData;\n const transaction = view.state.tr.setSelection(\n TextSelection.create(view.state.doc, from, to)\n );\n view.dispatch(transaction);\n openLinkMenu(view, href, text);\n\n return true;\n};\n\n/**\n * Regular expression for matching URLs, mailto links, phone links, and bare www-links\n */\nconst URL_REGEX =\n /(https?:\\/\\/[^\\s<>\"']+|mailto:[^\\s<>\"']+|tel:[^\\s<>\"']+|www\\.[^\\s<>\"']+)/g;\n\n/**\n * Checks if the text contains any URLs, mailto links, or phone links\n * @param text\n */\nconst hasUrls = (text: string): boolean => {\n // Reset regex before use\n URL_REGEX.lastIndex = 0;\n\n return URL_REGEX.test(text);\n};\n\n/**\n * Creates a text node with the provided content\n * @param schema\n * @param content\n */\nconst createTextNode = (schema: Schema, content: string): Node => {\n return schema.text(content);\n};\n\n/**\n * Creates a link node with the provided URL\n * @param schema\n * @param url\n */\nconst createLinkNode = (schema: Schema, url: string): Node => {\n const normalizeUrlForLinkMark = (input: string): string => {\n let output = input.trim();\n while (output.endsWith('\\\\')) {\n output = output.slice(0, -1);\n }\n if (output.toLowerCase().startsWith('www.')) {\n output = `https://${output}`;\n }\n return output;\n };\n\n const normalizedUrl = normalizeUrlForLinkMark(url);\n const linkMark = schema.marks.link.create(\n getLinkAttributes(normalizedUrl, normalizedUrl)\n );\n\n return schema.text(normalizedUrl, [linkMark]);\n};\n\n/**\n * Finds all link matches in the provided text\n * @param text\n */\nconst findLinkMatches = (\n text: string\n): Array<{ url: string; start: number; end: number }> => {\n const matches = [];\n let match: RegExpExecArray | null;\n\n // Reset regex before use\n URL_REGEX.lastIndex = 0;\n\n while ((match = URL_REGEX.exec(text)) !== null) {\n matches.push({\n url: match[0],\n start: match.index,\n end: match.index + match[0].length,\n });\n }\n\n return matches;\n};\n\n/**\n * Creates nodes for the pasted text while preserving soft line breaks.\n * - Each newline becomes a `hard_break`.\n * - Empty lines are preserved (consecutive newlines => multiple `hard_break`s).\n * - URLs inside each line are converted to link-marked text.\n * @param text - Raw pasted text\n * @param schema - ProseMirror schema\n */\nconst createNodesWithLinksAndBreaks = (\n text: string,\n schema: Schema\n): Node[] => {\n // Split preserves empty lines between consecutive newlines\n const lines = text.split(/\\r\\n|\\r|\\n/);\n const nodes: Node[] = [];\n\n for (const [index, line] of lines.entries()) {\n if (line.length > 0) {\n nodes.push(...createNodesWithLinks(line, schema));\n }\n if (index < lines.length - 1) {\n const hb = schema.nodes.hard_break;\n if (hb) {\n nodes.push(hb.create());\n } else {\n // Fallback: if schema lacks hard_break, defer to default paste behavior\n // (Do NOT throw; keep behavior stable across versions)\n console.warn('hard_break node not found in schema');\n }\n }\n }\n return nodes;\n};\n\n/**\n * Creates text nodes with links for any URLs, mailto links, or phone links found in the text\n * @param text\n * @param schema\n */\nconst createNodesWithLinks = (text: string, schema: Schema): Node[] => {\n const nodes: Node[] = [];\n const matches = findLinkMatches(text);\n\n if (matches.length === 0) {\n // No links found, just return the text as a single node\n return [createTextNode(schema, text)];\n }\n\n let lastIndex = 0;\n\n // Process each match\n for (const match of matches) {\n // Add text before the current link if any\n if (match.start > lastIndex) {\n nodes.push(\n createTextNode(schema, text.slice(lastIndex, match.start))\n );\n }\n\n // Add the link node\n nodes.push(createLinkNode(schema, match.url));\n\n lastIndex = match.end;\n }\n\n // Add any remaining text after the last link\n if (lastIndex < text.length) {\n nodes.push(createTextNode(schema, text.slice(lastIndex)));\n }\n\n return nodes;\n};\n\n/**\n * Pastes nodes at the current selection\n * @param view - The editor view\n * @param nodes - Array of nodes to paste\n */\nconst pasteAsLink = (view: EditorView, nodes: Node[]) => {\n if (nodes.length === 0) {\n return;\n }\n\n if (isSingleLinkNode(nodes)) {\n insertSingleLink(view, nodes[0]);\n } else {\n insertNodeFragment(view, nodes);\n }\n};\n\n/**\n * Checks if the nodes array contains just a single link node\n * @param nodes\n */\nconst isSingleLinkNode = (nodes: Node[]): boolean => {\n if (nodes.length !== 1) {\n return false;\n }\n\n const node = nodes[0];\n\n // Must be text with non-empty content\n if (!node.isText || !node.text || node.text.trim() === '') {\n return false;\n }\n\n // Must have a link mark (even if there are other marks, we just care about link presence)\n return node.marks.some((mark) => mark.type.name === 'link');\n};\n\n/**\n * Inserts a single link node, applying it to selected text if present\n * @param view\n * @param linkNode\n */\nconst insertSingleLink = (view: EditorView, linkNode: Node) => {\n const { state, dispatch } = view;\n const { from, to } = state.selection;\n\n const linkMark = linkNode.marks.find((mark) => mark.type.name === 'link');\n\n // Use selected text if there's a selection, otherwise use the URL\n const selectedText =\n state.doc.textBetween(from, to, ' ') || linkMark.attrs.href;\n\n // Insert the text and add the link mark\n dispatch(\n state.tr\n .insertText(selectedText, from, to)\n .addMark(from, from + selectedText.length, linkMark)\n );\n};\n\n/**\n * Inserts multiple nodes as a fragment at the current selection\n * @param view - The editor view\n * @param nodes - Array of nodes to insert\n */\nconst insertNodeFragment = (view: EditorView, nodes: Node[]) => {\n const { state, dispatch } = view;\n const { from, to } = state.selection;\n\n // Create a fragment from the array of nodes\n const fragment = Fragment.fromArray(nodes);\n\n // Replace the current selection with the fragment\n dispatch(state.tr.replaceWith(from, to, fragment));\n};\n\n/**\n * Handles pasted content, converting URLs to links\n * @param view\n * @param event\n */\nconst processPasteEvent = (\n view: EditorView,\n event: ClipboardEvent\n): boolean => {\n const text = event.clipboardData?.getData('text/plain');\n\n if (!text || !hasUrls(text)) {\n return false;\n }\n\n const nodes = createNodesWithLinksAndBreaks(text, view.state.schema);\n\n event.preventDefault();\n pasteAsLink(view, nodes);\n\n return true;\n};\n\nexport const createLinkPlugin = (updateLinkCallback?: UpdateLinkCallback) => {\n return new Plugin({\n key: linkPluginKey,\n props: {\n handlePaste: (view, event) => {\n return processPasteEvent(view, event);\n },\n handleDOMEvents: {\n mousedown: (view, event) => {\n if (\n (event.metaKey || event.ctrlKey) &&\n event.button === 0\n ) {\n return processModClickEvent(view, event);\n }\n },\n dblclick: (view, event) => {\n if (event.button !== MouseButtons.Right) {\n // We want to ignore right-clicks\n return processDoubleClickEvent(view, event);\n }\n },\n click: (_view, event) => {\n if (!(event.target instanceof HTMLElement)) {\n return;\n }\n\n // Prevent unhandled navigation and bubbling for link clicks\n const link = event.target.closest('a');\n if (link) {\n event.preventDefault();\n event.stopPropagation();\n }\n },\n },\n },\n view: () => ({\n update: (view) => {\n updateLink(view, updateLinkCallback);\n },\n }),\n });\n};\n"]}
|
|
@@ -26188,9 +26188,14 @@ const getLinkDataAtPosition = (view, event) => {
|
|
|
26188
26188
|
return { href: href, text: text, from: from, to: to };
|
|
26189
26189
|
};
|
|
26190
26190
|
const processModClickEvent = (view, event) => {
|
|
26191
|
-
const
|
|
26191
|
+
const linkData = getLinkDataAtPosition(view, event);
|
|
26192
|
+
if (!linkData.href) {
|
|
26193
|
+
return false;
|
|
26194
|
+
}
|
|
26195
|
+
event.preventDefault();
|
|
26196
|
+
const { href } = linkData;
|
|
26192
26197
|
if (href) {
|
|
26193
|
-
window.open(href, '_blank');
|
|
26198
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
26194
26199
|
return true;
|
|
26195
26200
|
}
|
|
26196
26201
|
return false;
|
|
@@ -26203,32 +26208,21 @@ const openLinkMenu = (view, href, text) => {
|
|
|
26203
26208
|
});
|
|
26204
26209
|
view.dom.dispatchEvent(event);
|
|
26205
26210
|
};
|
|
26206
|
-
|
|
26207
|
-
const
|
|
26208
|
-
|
|
26209
|
-
const processClickEvent = (view, event) => {
|
|
26210
|
-
const now = Date.now();
|
|
26211
|
-
if (now - lastClickTime < DOUBLE_CLICK_DELAY) {
|
|
26212
|
-
clearTimeout(clickTimeout);
|
|
26213
|
-
lastClickTime = now; // Reset lastClickTime to prevent single-click action
|
|
26211
|
+
const processDoubleClickEvent = (view, event) => {
|
|
26212
|
+
const linkData = getLinkDataAtPosition(view, event);
|
|
26213
|
+
if (!linkData) {
|
|
26214
26214
|
return false;
|
|
26215
26215
|
}
|
|
26216
|
-
|
|
26217
|
-
|
|
26218
|
-
|
|
26219
|
-
|
|
26220
|
-
const { href, text, from, to } = linkData;
|
|
26221
|
-
const transaction = view.state.tr.setSelection(TextSelection.create(view.state.doc, from, to));
|
|
26222
|
-
view.dispatch(transaction);
|
|
26223
|
-
openLinkMenu(view, href, text);
|
|
26224
|
-
}
|
|
26225
|
-
}, DOUBLE_CLICK_DELAY);
|
|
26216
|
+
const { href, text, from, to } = linkData;
|
|
26217
|
+
const transaction = view.state.tr.setSelection(TextSelection.create(view.state.doc, from, to));
|
|
26218
|
+
view.dispatch(transaction);
|
|
26219
|
+
openLinkMenu(view, href, text);
|
|
26226
26220
|
return true;
|
|
26227
26221
|
};
|
|
26228
26222
|
/**
|
|
26229
|
-
* Regular expression for matching URLs, mailto links, and
|
|
26223
|
+
* Regular expression for matching URLs, mailto links, phone links, and bare www-links
|
|
26230
26224
|
*/
|
|
26231
|
-
const URL_REGEX = /(https?:\/\/[^\s<>"']+|mailto:[^\s<>"']+|tel:[^\s<>"']+)/g;
|
|
26225
|
+
const URL_REGEX = /(https?:\/\/[^\s<>"']+|mailto:[^\s<>"']+|tel:[^\s<>"']+|www\.[^\s<>"']+)/g;
|
|
26232
26226
|
/**
|
|
26233
26227
|
* Checks if the text contains any URLs, mailto links, or phone links
|
|
26234
26228
|
* @param text
|
|
@@ -26252,8 +26246,19 @@ const createTextNode = (schema, content) => {
|
|
|
26252
26246
|
* @param url
|
|
26253
26247
|
*/
|
|
26254
26248
|
const createLinkNode = (schema, url) => {
|
|
26255
|
-
const
|
|
26256
|
-
|
|
26249
|
+
const normalizeUrlForLinkMark = (input) => {
|
|
26250
|
+
let output = input.trim();
|
|
26251
|
+
while (output.endsWith('\\')) {
|
|
26252
|
+
output = output.slice(0, -1);
|
|
26253
|
+
}
|
|
26254
|
+
if (output.toLowerCase().startsWith('www.')) {
|
|
26255
|
+
output = `https://${output}`;
|
|
26256
|
+
}
|
|
26257
|
+
return output;
|
|
26258
|
+
};
|
|
26259
|
+
const normalizedUrl = normalizeUrlForLinkMark(url);
|
|
26260
|
+
const linkMark = schema.marks.link.create(getLinkAttributes(normalizedUrl, normalizedUrl));
|
|
26261
|
+
return schema.text(normalizedUrl, [linkMark]);
|
|
26257
26262
|
};
|
|
26258
26263
|
/**
|
|
26259
26264
|
* Finds all link matches in the provided text
|
|
@@ -26273,6 +26278,36 @@ const findLinkMatches = (text) => {
|
|
|
26273
26278
|
}
|
|
26274
26279
|
return matches;
|
|
26275
26280
|
};
|
|
26281
|
+
/**
|
|
26282
|
+
* Creates nodes for the pasted text while preserving soft line breaks.
|
|
26283
|
+
* - Each newline becomes a `hard_break`.
|
|
26284
|
+
* - Empty lines are preserved (consecutive newlines => multiple `hard_break`s).
|
|
26285
|
+
* - URLs inside each line are converted to link-marked text.
|
|
26286
|
+
* @param text - Raw pasted text
|
|
26287
|
+
* @param schema - ProseMirror schema
|
|
26288
|
+
*/
|
|
26289
|
+
const createNodesWithLinksAndBreaks = (text, schema) => {
|
|
26290
|
+
// Split preserves empty lines between consecutive newlines
|
|
26291
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
26292
|
+
const nodes = [];
|
|
26293
|
+
for (const [index, line] of lines.entries()) {
|
|
26294
|
+
if (line.length > 0) {
|
|
26295
|
+
nodes.push(...createNodesWithLinks(line, schema));
|
|
26296
|
+
}
|
|
26297
|
+
if (index < lines.length - 1) {
|
|
26298
|
+
const hb = schema.nodes.hard_break;
|
|
26299
|
+
if (hb) {
|
|
26300
|
+
nodes.push(hb.create());
|
|
26301
|
+
}
|
|
26302
|
+
else {
|
|
26303
|
+
// Fallback: if schema lacks hard_break, defer to default paste behavior
|
|
26304
|
+
// (Do NOT throw; keep behavior stable across versions)
|
|
26305
|
+
console.warn('hard_break node not found in schema');
|
|
26306
|
+
}
|
|
26307
|
+
}
|
|
26308
|
+
}
|
|
26309
|
+
return nodes;
|
|
26310
|
+
};
|
|
26276
26311
|
/**
|
|
26277
26312
|
* Creates text nodes with links for any URLs, mailto links, or phone links found in the text
|
|
26278
26313
|
* @param text
|
|
@@ -26374,7 +26409,8 @@ const processPasteEvent$1 = (view, event) => {
|
|
|
26374
26409
|
if (!text || !hasUrls(text)) {
|
|
26375
26410
|
return false;
|
|
26376
26411
|
}
|
|
26377
|
-
const nodes =
|
|
26412
|
+
const nodes = createNodesWithLinksAndBreaks(text, view.state.schema);
|
|
26413
|
+
event.preventDefault();
|
|
26378
26414
|
pasteAsLink(view, nodes);
|
|
26379
26415
|
return true;
|
|
26380
26416
|
};
|
|
@@ -26391,11 +26427,12 @@ const createLinkPlugin = (updateLinkCallback) => {
|
|
|
26391
26427
|
event.button === 0) {
|
|
26392
26428
|
return processModClickEvent(view, event);
|
|
26393
26429
|
}
|
|
26430
|
+
},
|
|
26431
|
+
dblclick: (view, event) => {
|
|
26394
26432
|
if (event.button !== MouseButtons.Right) {
|
|
26395
26433
|
// We want to ignore right-clicks
|
|
26396
|
-
return
|
|
26434
|
+
return processDoubleClickEvent(view, event);
|
|
26397
26435
|
}
|
|
26398
|
-
return true;
|
|
26399
26436
|
},
|
|
26400
26437
|
click: (_view, event) => {
|
|
26401
26438
|
if (!(event.target instanceof HTMLElement)) {
|