overtype 1.1.0 → 1.1.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/dist/overtype.esm.js +72 -1297
- package/dist/overtype.esm.js.map +4 -4
- package/dist/overtype.js +72 -1297
- package/dist/overtype.js.map +4 -4
- package/dist/overtype.min.js +83 -70
- package/package.json +1 -2
- package/src/link-tooltip.js +75 -149
- package/src/parser.js +17 -1
package/src/link-tooltip.js
CHANGED
|
@@ -1,73 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Link Tooltip -
|
|
2
|
+
* Link Tooltip - CSS Anchor Positioning with index-based anchors
|
|
3
3
|
* Shows a clickable tooltip when cursor is within a link
|
|
4
|
+
* Uses CSS anchor positioning with dynamically selected anchor
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
|
|
7
|
-
|
|
8
7
|
export class LinkTooltip {
|
|
9
8
|
constructor(editor) {
|
|
10
9
|
this.editor = editor;
|
|
11
10
|
this.tooltip = null;
|
|
12
11
|
this.currentLink = null;
|
|
13
12
|
this.hideTimeout = null;
|
|
14
|
-
this.isMouseInTooltip = false;
|
|
15
|
-
this.isMouseInLink = false;
|
|
16
13
|
|
|
17
14
|
this.init();
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
init() {
|
|
18
|
+
// Check for CSS anchor positioning support
|
|
19
|
+
const supportsAnchor =
|
|
20
|
+
CSS.supports('position-anchor: --x') &&
|
|
21
|
+
CSS.supports('position-area: center');
|
|
22
|
+
|
|
23
|
+
if (!supportsAnchor) {
|
|
24
|
+
// Don't show anything if not supported
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
// Create tooltip element
|
|
22
29
|
this.createTooltip();
|
|
23
30
|
|
|
24
31
|
// Listen for cursor position changes
|
|
25
32
|
this.editor.textarea.addEventListener('selectionchange', () => this.checkCursorPosition());
|
|
26
|
-
this.editor.textarea.addEventListener('input', () => this.checkCursorPosition());
|
|
27
33
|
this.editor.textarea.addEventListener('keyup', (e) => {
|
|
28
|
-
|
|
29
|
-
if (e.key.includes('Arrow')) {
|
|
34
|
+
if (e.key.includes('Arrow') || e.key === 'Home' || e.key === 'End') {
|
|
30
35
|
this.checkCursorPosition();
|
|
31
36
|
}
|
|
32
37
|
});
|
|
33
38
|
|
|
34
|
-
// Hide tooltip when scrolling
|
|
39
|
+
// Hide tooltip when typing or scrolling
|
|
40
|
+
this.editor.textarea.addEventListener('input', () => this.hide());
|
|
35
41
|
this.editor.textarea.addEventListener('scroll', () => this.hide());
|
|
36
42
|
|
|
37
|
-
//
|
|
38
|
-
this.tooltip.addEventListener('mouseenter', () =>
|
|
39
|
-
|
|
40
|
-
this.cancelHide();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
this.tooltip.addEventListener('mouseleave', () => {
|
|
44
|
-
this.isMouseInTooltip = false;
|
|
45
|
-
this.scheduleHide();
|
|
46
|
-
});
|
|
43
|
+
// Keep tooltip visible on hover
|
|
44
|
+
this.tooltip.addEventListener('mouseenter', () => this.cancelHide());
|
|
45
|
+
this.tooltip.addEventListener('mouseleave', () => this.scheduleHide());
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
createTooltip() {
|
|
49
|
+
// Create tooltip element
|
|
50
50
|
this.tooltip = document.createElement('div');
|
|
51
51
|
this.tooltip.className = 'overtype-link-tooltip';
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
52
|
+
|
|
53
|
+
// Add CSS anchor positioning styles
|
|
54
|
+
const tooltipStyles = document.createElement('style');
|
|
55
|
+
tooltipStyles.textContent = `
|
|
56
|
+
@supports (position-anchor: --x) and (position-area: center) {
|
|
57
|
+
.overtype-link-tooltip {
|
|
58
|
+
position: absolute;
|
|
59
|
+
position-anchor: var(--target-anchor, --link-0);
|
|
60
|
+
position-area: block-end center;
|
|
61
|
+
margin-top: 8px;
|
|
62
|
+
|
|
63
|
+
background: #333;
|
|
64
|
+
color: white;
|
|
65
|
+
padding: 6px 10px;
|
|
66
|
+
border-radius: 16px;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
69
|
+
display: none;
|
|
70
|
+
z-index: 10000;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
73
|
+
max-width: 300px;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
text-overflow: ellipsis;
|
|
77
|
+
|
|
78
|
+
position-try: most-width block-end inline-end, flip-inline, block-start center;
|
|
79
|
+
position-visibility: anchors-visible;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.overtype-link-tooltip.visible {
|
|
83
|
+
display: flex;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
`;
|
|
87
|
+
document.head.appendChild(tooltipStyles);
|
|
71
88
|
|
|
72
89
|
// Add link icon and text container
|
|
73
90
|
this.tooltip.innerHTML = `
|
|
@@ -90,8 +107,8 @@ export class LinkTooltip {
|
|
|
90
107
|
}
|
|
91
108
|
});
|
|
92
109
|
|
|
93
|
-
// Append to
|
|
94
|
-
|
|
110
|
+
// Append tooltip to editor container
|
|
111
|
+
this.editor.container.appendChild(this.tooltip);
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
checkCursorPosition() {
|
|
@@ -99,19 +116,13 @@ export class LinkTooltip {
|
|
|
99
116
|
const text = this.editor.textarea.value;
|
|
100
117
|
|
|
101
118
|
// Find if cursor is within a markdown link
|
|
102
|
-
const
|
|
119
|
+
const linkInfo = this.findLinkAtPosition(text, cursorPos);
|
|
103
120
|
|
|
104
|
-
if (
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
this.currentLink.start !== link.start ||
|
|
108
|
-
this.currentLink.url !== link.url) {
|
|
109
|
-
// New link or different link
|
|
110
|
-
this.show(link);
|
|
121
|
+
if (linkInfo) {
|
|
122
|
+
if (!this.currentLink || this.currentLink.url !== linkInfo.url || this.currentLink.index !== linkInfo.index) {
|
|
123
|
+
this.show(linkInfo);
|
|
111
124
|
}
|
|
112
125
|
} else {
|
|
113
|
-
// Not in a link
|
|
114
|
-
this.isMouseInLink = false;
|
|
115
126
|
this.scheduleHide();
|
|
116
127
|
}
|
|
117
128
|
}
|
|
@@ -120,6 +131,7 @@ export class LinkTooltip {
|
|
|
120
131
|
// Regex to find markdown links: [text](url)
|
|
121
132
|
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
122
133
|
let match;
|
|
134
|
+
let linkIndex = 0;
|
|
123
135
|
|
|
124
136
|
while ((match = linkRegex.exec(text)) !== null) {
|
|
125
137
|
const start = match.index;
|
|
@@ -127,128 +139,42 @@ export class LinkTooltip {
|
|
|
127
139
|
|
|
128
140
|
if (position >= start && position <= end) {
|
|
129
141
|
return {
|
|
130
|
-
start: start,
|
|
131
|
-
end: end,
|
|
132
142
|
text: match[1],
|
|
133
143
|
url: match[2],
|
|
134
|
-
|
|
144
|
+
index: linkIndex,
|
|
145
|
+
start: start,
|
|
146
|
+
end: end
|
|
135
147
|
};
|
|
136
148
|
}
|
|
149
|
+
linkIndex++;
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
return null;
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
this.currentLink =
|
|
155
|
+
show(linkInfo) {
|
|
156
|
+
this.currentLink = linkInfo;
|
|
144
157
|
this.cancelHide();
|
|
145
158
|
|
|
146
159
|
// Update tooltip content
|
|
147
160
|
const urlSpan = this.tooltip.querySelector('.overtype-link-tooltip-url');
|
|
148
|
-
urlSpan.textContent =
|
|
161
|
+
urlSpan.textContent = linkInfo.url;
|
|
149
162
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (linkElement) {
|
|
154
|
-
// Use the link element as reference
|
|
155
|
-
await this.positionTooltip(linkElement);
|
|
156
|
-
} else {
|
|
157
|
-
// Fallback: position based on cursor
|
|
158
|
-
await this.positionTooltipAtCursor(link);
|
|
159
|
-
}
|
|
163
|
+
// Set the CSS variable to point to the correct anchor
|
|
164
|
+
this.tooltip.style.setProperty('--target-anchor', `--link-${linkInfo.index}`);
|
|
160
165
|
|
|
161
|
-
// Show tooltip
|
|
162
|
-
this.tooltip.
|
|
163
|
-
// Force reflow
|
|
164
|
-
this.tooltip.offsetHeight;
|
|
165
|
-
this.tooltip.style.opacity = '1';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
findLinkElementInPreview(link) {
|
|
169
|
-
// Find the corresponding link element in the preview
|
|
170
|
-
const links = this.editor.preview.querySelectorAll('a');
|
|
171
|
-
|
|
172
|
-
for (const linkEl of links) {
|
|
173
|
-
// Check if this link contains our URL
|
|
174
|
-
const urlSpans = linkEl.querySelectorAll('.syntax-marker');
|
|
175
|
-
for (const span of urlSpans) {
|
|
176
|
-
if (span.textContent === link.url) {
|
|
177
|
-
return linkEl;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async positionTooltip(referenceEl) {
|
|
186
|
-
const { x, y } = await computePosition(referenceEl, this.tooltip, {
|
|
187
|
-
placement: 'bottom',
|
|
188
|
-
middleware: [
|
|
189
|
-
offset(6),
|
|
190
|
-
flip(),
|
|
191
|
-
shift({ padding: 10 })
|
|
192
|
-
]
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
Object.assign(this.tooltip.style, {
|
|
196
|
-
left: `${x}px`,
|
|
197
|
-
top: `${y}px`
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async positionTooltipAtCursor(link) {
|
|
202
|
-
// Get cursor position in the textarea
|
|
203
|
-
const textarea = this.editor.textarea;
|
|
204
|
-
|
|
205
|
-
// Create a temporary element to measure text position
|
|
206
|
-
const measurer = document.createElement('div');
|
|
207
|
-
measurer.style.cssText = window.getComputedStyle(textarea).cssText;
|
|
208
|
-
measurer.style.position = 'absolute';
|
|
209
|
-
measurer.style.visibility = 'hidden';
|
|
210
|
-
measurer.style.whiteSpace = 'pre-wrap';
|
|
211
|
-
measurer.style.wordWrap = 'break-word';
|
|
212
|
-
|
|
213
|
-
// Get text up to cursor
|
|
214
|
-
const textBeforeCursor = textarea.value.substring(0, link.start + link.fullMatch.length / 2);
|
|
215
|
-
measurer.textContent = textBeforeCursor;
|
|
216
|
-
|
|
217
|
-
document.body.appendChild(measurer);
|
|
218
|
-
const textHeight = measurer.offsetHeight;
|
|
219
|
-
document.body.removeChild(measurer);
|
|
220
|
-
|
|
221
|
-
// Get textarea position
|
|
222
|
-
const rect = textarea.getBoundingClientRect();
|
|
223
|
-
|
|
224
|
-
// Estimate position (this is approximate)
|
|
225
|
-
const x = rect.left + rect.width / 2;
|
|
226
|
-
const y = rect.top + Math.min(textHeight, rect.height - 50);
|
|
227
|
-
|
|
228
|
-
Object.assign(this.tooltip.style, {
|
|
229
|
-
left: `${x}px`,
|
|
230
|
-
top: `${y}px`,
|
|
231
|
-
transform: 'translateX(-50%)'
|
|
232
|
-
});
|
|
166
|
+
// Show tooltip (CSS anchor positioning handles the rest)
|
|
167
|
+
this.tooltip.classList.add('visible');
|
|
233
168
|
}
|
|
234
169
|
|
|
235
170
|
hide() {
|
|
236
|
-
this.tooltip.
|
|
237
|
-
|
|
238
|
-
if (this.tooltip.style.opacity === '0') {
|
|
239
|
-
this.tooltip.style.display = 'none';
|
|
240
|
-
this.currentLink = null;
|
|
241
|
-
}
|
|
242
|
-
}, 200);
|
|
171
|
+
this.tooltip.classList.remove('visible');
|
|
172
|
+
this.currentLink = null;
|
|
243
173
|
}
|
|
244
174
|
|
|
245
175
|
scheduleHide() {
|
|
246
176
|
this.cancelHide();
|
|
247
|
-
this.hideTimeout = setTimeout(() =>
|
|
248
|
-
if (!this.isMouseInTooltip && !this.isMouseInLink) {
|
|
249
|
-
this.hide();
|
|
250
|
-
}
|
|
251
|
-
}, 300);
|
|
177
|
+
this.hideTimeout = setTimeout(() => this.hide(), 300);
|
|
252
178
|
}
|
|
253
179
|
|
|
254
180
|
cancelHide() {
|
package/src/parser.js
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* - Markdown tokens remain visible but styled
|
|
8
8
|
*/
|
|
9
9
|
export class MarkdownParser {
|
|
10
|
+
// Track link index for anchor naming
|
|
11
|
+
static linkIndex = 0;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reset link index (call before parsing a new document)
|
|
15
|
+
*/
|
|
16
|
+
static resetLinkIndex() {
|
|
17
|
+
this.linkIndex = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* Escape HTML special characters
|
|
12
22
|
* @param {string} text - Raw text to escape
|
|
@@ -143,7 +153,10 @@ export class MarkdownParser {
|
|
|
143
153
|
* @returns {string} HTML with link styling
|
|
144
154
|
*/
|
|
145
155
|
static parseLinks(html) {
|
|
146
|
-
return html.replace(/\[(.+?)\]\((.+?)\)/g,
|
|
156
|
+
return html.replace(/\[(.+?)\]\((.+?)\)/g, (match, text, url) => {
|
|
157
|
+
const anchorName = `--link-${this.linkIndex++}`;
|
|
158
|
+
return `<a href="${url}" style="anchor-name: ${anchorName}"><span class="syntax-marker">[</span>${text}<span class="syntax-marker">](</span><span class="syntax-marker">${url}</span><span class="syntax-marker">)</span></a>`;
|
|
159
|
+
});
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
/**
|
|
@@ -223,6 +236,9 @@ export class MarkdownParser {
|
|
|
223
236
|
* @returns {string} Parsed HTML
|
|
224
237
|
*/
|
|
225
238
|
static parse(text, activeLine = -1, showActiveLineRaw = false) {
|
|
239
|
+
// Reset link counter for each parse
|
|
240
|
+
this.resetLinkIndex();
|
|
241
|
+
|
|
226
242
|
const lines = text.split('\n');
|
|
227
243
|
const parsedLines = lines.map((line, index) => {
|
|
228
244
|
// Show raw markdown on active line if requested
|