overtype 1.1.0 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtype",
3
- "version": "1.1.0",
3
+ "version": "1.1.3",
4
4
  "description": "A lightweight markdown editor library with perfect WYSIWYG alignment using an invisible textarea overlay",
5
5
  "main": "dist/overtype.js",
6
6
  "module": "dist/overtype.esm.js",
@@ -46,7 +46,6 @@
46
46
  },
47
47
  "homepage": "https://github.com/panphora/overtype#readme",
48
48
  "dependencies": {
49
- "@floating-ui/dom": "^1.7.3",
50
49
  "markdown-actions": "^1.1.2"
51
50
  }
52
51
  }
@@ -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/overtype.js CHANGED
@@ -144,8 +144,14 @@ class OverType {
144
144
  lineHeight: 1.5
145
145
  },
146
146
 
147
+ // Native textarea properties
148
+ textareaProps: {},
149
+
147
150
  // Behavior
148
151
  autofocus: false,
152
+ autoResize: false, // Auto-expand height with content
153
+ minHeight: '100px', // Minimum height for autoResize mode
154
+ maxHeight: null, // Maximum height for autoResize mode (null = unlimited)
149
155
  placeholder: 'Start typing...',
150
156
  value: '',
151
157
 
@@ -307,10 +313,6 @@ class OverType {
307
313
  this.wrapper = document.createElement('div');
308
314
  this.wrapper.className = 'overtype-wrapper';
309
315
 
310
- // Add stats wrapper class if stats are enabled
311
- if (this.options.showStats) {
312
- this.wrapper.classList.add('with-stats');
313
- }
314
316
 
315
317
  // Apply instance-specific styles via CSS custom properties
316
318
  if (this.options.fontSize) {
@@ -330,6 +332,19 @@ class OverType {
330
332
  this.textarea.className = 'overtype-input';
331
333
  this.textarea.placeholder = this.options.placeholder;
332
334
  this._configureTextarea();
335
+
336
+ // Apply any native textarea properties
337
+ if (this.options.textareaProps) {
338
+ Object.entries(this.options.textareaProps).forEach(([key, value]) => {
339
+ if (key === 'className' || key === 'class') {
340
+ this.textarea.className += ' ' + value;
341
+ } else if (key === 'style' && typeof value === 'object') {
342
+ Object.assign(this.textarea.style, value);
343
+ } else {
344
+ this.textarea.setAttribute(key, value);
345
+ }
346
+ });
347
+ }
333
348
 
334
349
  // Create preview div
335
350
  this.preview = document.createElement('div');
@@ -340,19 +355,42 @@ class OverType {
340
355
  this.wrapper.appendChild(this.textarea);
341
356
  this.wrapper.appendChild(this.preview);
342
357
 
343
- // Add stats bar if enabled
358
+ // Add wrapper to container first
359
+ this.container.appendChild(this.wrapper);
360
+
361
+ // Add stats bar at the end (bottom) if enabled
344
362
  if (this.options.showStats) {
345
363
  this.statsBar = document.createElement('div');
346
364
  this.statsBar.className = 'overtype-stats';
347
- this.wrapper.appendChild(this.statsBar);
365
+ this.container.appendChild(this.statsBar);
348
366
  this._updateStats();
349
367
  }
350
368
 
351
- // Add wrapper to container
352
- this.container.appendChild(this.wrapper);
353
-
354
369
  // Add container to element
355
370
  this.element.appendChild(this.container);
371
+
372
+ // Debug logging
373
+ if (window.location.pathname.includes('demo.html')) {
374
+ console.log('_createDOM completed:', {
375
+ elementId: this.element.id,
376
+ autoResize: this.options.autoResize,
377
+ containerClasses: this.container.className,
378
+ hasStats: !!this.statsBar,
379
+ hasToolbar: this.options.toolbar
380
+ });
381
+ }
382
+
383
+ // Setup auto-resize if enabled
384
+ if (this.options.autoResize) {
385
+ this._setupAutoResize();
386
+ } else {
387
+ // Ensure auto-resize class is removed if not using auto-resize
388
+ this.container.classList.remove('overtype-auto-resize');
389
+
390
+ if (window.location.pathname.includes('demo.html')) {
391
+ console.log('Removed auto-resize class from:', this.element.id);
392
+ }
393
+ }
356
394
  }
357
395
 
358
396
  /**
@@ -378,6 +416,16 @@ class OverType {
378
416
  if (this.options.autofocus) {
379
417
  this.textarea.focus();
380
418
  }
419
+
420
+ // Setup or remove auto-resize
421
+ if (this.options.autoResize) {
422
+ if (!this.container.classList.contains('overtype-auto-resize')) {
423
+ this._setupAutoResize();
424
+ }
425
+ } else {
426
+ // Ensure auto-resize class is removed
427
+ this.container.classList.remove('overtype-auto-resize');
428
+ }
381
429
 
382
430
  // Update preview with initial content
383
431
  this.updatePreview();
@@ -554,6 +602,11 @@ class OverType {
554
602
  setValue(value) {
555
603
  this.textarea.value = value;
556
604
  this.updatePreview();
605
+
606
+ // Update height if auto-resize is enabled
607
+ if (this.options.autoResize) {
608
+ this._updateAutoHeight();
609
+ }
557
610
  }
558
611
 
559
612
 
@@ -629,6 +682,89 @@ class OverType {
629
682
  }
630
683
  }
631
684
 
685
+ /**
686
+ * Setup auto-resize functionality
687
+ * @private
688
+ */
689
+ _setupAutoResize() {
690
+ // Add auto-resize class for styling
691
+ this.container.classList.add('overtype-auto-resize');
692
+
693
+ // Store previous height for comparison
694
+ this.previousHeight = null;
695
+
696
+ // Initial height update
697
+ this._updateAutoHeight();
698
+
699
+ // Listen for input events
700
+ this.textarea.addEventListener('input', () => this._updateAutoHeight());
701
+
702
+ // Listen for window resize
703
+ window.addEventListener('resize', () => this._updateAutoHeight());
704
+ }
705
+
706
+ /**
707
+ * Update height based on scrollHeight
708
+ * @private
709
+ */
710
+ _updateAutoHeight() {
711
+ if (!this.options.autoResize) return;
712
+
713
+ const textarea = this.textarea;
714
+ const preview = this.preview;
715
+ const wrapper = this.wrapper;
716
+
717
+ // Get computed styles
718
+ const computed = window.getComputedStyle(textarea);
719
+ const paddingTop = parseFloat(computed.paddingTop);
720
+ const paddingBottom = parseFloat(computed.paddingBottom);
721
+
722
+ // Store scroll positions
723
+ const scrollTop = textarea.scrollTop;
724
+
725
+ // Reset height to get accurate scrollHeight
726
+ textarea.style.setProperty('height', 'auto', 'important');
727
+
728
+ // Calculate new height based on scrollHeight
729
+ let newHeight = textarea.scrollHeight;
730
+
731
+ // Apply min height constraint
732
+ if (this.options.minHeight) {
733
+ const minHeight = parseInt(this.options.minHeight);
734
+ newHeight = Math.max(newHeight, minHeight);
735
+ }
736
+
737
+ // Apply max height constraint
738
+ let overflow = 'hidden';
739
+ if (this.options.maxHeight) {
740
+ const maxHeight = parseInt(this.options.maxHeight);
741
+ if (newHeight > maxHeight) {
742
+ newHeight = maxHeight;
743
+ overflow = 'auto';
744
+ }
745
+ }
746
+
747
+ // Apply the new height to all elements with !important to override base styles
748
+ const heightPx = newHeight + 'px';
749
+ textarea.style.setProperty('height', heightPx, 'important');
750
+ textarea.style.setProperty('overflow-y', overflow, 'important');
751
+
752
+ preview.style.setProperty('height', heightPx, 'important');
753
+ preview.style.setProperty('overflow-y', overflow, 'important');
754
+
755
+ wrapper.style.setProperty('height', heightPx, 'important');
756
+
757
+ // Restore scroll position
758
+ textarea.scrollTop = scrollTop;
759
+ preview.scrollTop = scrollTop;
760
+
761
+ // Track if height changed
762
+ if (this.previousHeight !== newHeight) {
763
+ this.previousHeight = newHeight;
764
+ // Could dispatch a custom event here if needed
765
+ }
766
+ }
767
+
632
768
  /**
633
769
  * Show or hide stats bar
634
770
  * @param {boolean} show - Whether to show stats
@@ -637,17 +773,15 @@ class OverType {
637
773
  this.options.showStats = show;
638
774
 
639
775
  if (show && !this.statsBar) {
640
- // Create stats bar
776
+ // Create stats bar (add to container, not wrapper)
641
777
  this.statsBar = document.createElement('div');
642
778
  this.statsBar.className = 'overtype-stats';
643
- this.wrapper.appendChild(this.statsBar);
644
- this.wrapper.classList.add('with-stats');
779
+ this.container.appendChild(this.statsBar);
645
780
  this._updateStats();
646
781
  } else if (!show && this.statsBar) {
647
782
  // Remove stats bar
648
783
  this.statsBar.remove();
649
784
  this.statsBar = null;
650
- this.wrapper.classList.remove('with-stats');
651
785
  }
652
786
  }
653
787
 
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
@@ -99,7 +109,9 @@ export class MarkdownParser {
99
109
  * @returns {string|null} Parsed code fence or null
100
110
  */
101
111
  static parseCodeBlock(html) {
102
- if (html.startsWith('```')) {
112
+ // Only treat as code block if ``` is alone or followed by a language identifier
113
+ // This prevents ```some code``` from being treated as a code fence
114
+ if (html.match(/^```(\s*|\w*)$/)) {
103
115
  return `<div><span class="code-fence">${html}</span></div>`;
104
116
  }
105
117
  return null;
@@ -143,7 +155,10 @@ export class MarkdownParser {
143
155
  * @returns {string} HTML with link styling
144
156
  */
145
157
  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>');
158
+ return html.replace(/\[(.+?)\]\((.+?)\)/g, (match, text, url) => {
159
+ const anchorName = `--link-${this.linkIndex++}`;
160
+ 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>`;
161
+ });
147
162
  }
148
163
 
149
164
  /**
@@ -223,6 +238,9 @@ export class MarkdownParser {
223
238
  * @returns {string} Parsed HTML
224
239
  */
225
240
  static parse(text, activeLine = -1, showActiveLineRaw = false) {
241
+ // Reset link counter for each parse
242
+ this.resetLinkIndex();
243
+
226
244
  const lines = text.split('\n');
227
245
  const parsedLines = lines.map((line, index) => {
228
246
  // Show raw markdown on active line if requested