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.
@@ -1,73 +1,90 @@
1
1
  /**
2
- * Link Tooltip - Gmail/Google Docs style link preview
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
- // Arrow keys might move cursor
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
- // Mouse events for tooltip persistence
38
- this.tooltip.addEventListener('mouseenter', () => {
39
- this.isMouseInTooltip = true;
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
- this.tooltip.style.cssText = `
53
- position: absolute;
54
- background: #333;
55
- color: white;
56
- padding: 6px 10px;
57
- border-radius: 16px;
58
- font-size: 12px;
59
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
60
- display: none;
61
- z-index: 10000;
62
- cursor: pointer;
63
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
64
- max-width: 300px;
65
- white-space: nowrap;
66
- overflow: hidden;
67
- text-overflow: ellipsis;
68
- transition: opacity 0.2s;
69
- opacity: 0;
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 document body for proper positioning
94
- document.body.appendChild(this.tooltip);
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 link = this.findLinkAtPosition(text, cursorPos);
119
+ const linkInfo = this.findLinkAtPosition(text, cursorPos);
103
120
 
104
- if (link) {
105
- this.isMouseInLink = true;
106
- if (!this.currentLink ||
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
- fullMatch: match[0]
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
- async show(link) {
143
- this.currentLink = link;
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 = link.url;
161
+ urlSpan.textContent = linkInfo.url;
149
162
 
150
- // Get the position of the link in the preview
151
- const linkElement = this.findLinkElementInPreview(link);
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 with animation
162
- this.tooltip.style.display = 'block';
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.style.opacity = '0';
237
- setTimeout(() => {
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, '<a href="$2"><span class="syntax-marker">[</span>$1<span class="syntax-marker">](</span><span class="syntax-marker">$2</span><span class="syntax-marker">)</span></a>');
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