overtype 2.1.1 → 2.3.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.
package/src/overtype.js CHANGED
@@ -7,10 +7,10 @@
7
7
  import { MarkdownParser } from './parser.js';
8
8
  import { ShortcutsManager } from './shortcuts.js';
9
9
  import { generateStyles } from './styles.js';
10
- import { getTheme, mergeTheme, solar, themeToCSSVars } from './themes.js';
10
+ import { getTheme, mergeTheme, solar, themeToCSSVars, resolveAutoTheme } from './themes.js';
11
11
  import { Toolbar } from './toolbar.js';
12
12
  import { LinkTooltip } from './link-tooltip.js';
13
- import { defaultToolbarButtons } from './toolbar-buttons.js';
13
+ import { defaultToolbarButtons, toolbarButtons as builtinToolbarButtons } from './toolbar-buttons.js';
14
14
 
15
15
  /**
16
16
  * Build action map from toolbar button configurations
@@ -80,6 +80,11 @@ class OverType {
80
80
  static stylesInjected = false;
81
81
  static globalListenersInitialized = false;
82
82
  static instanceCount = 0;
83
+ static _autoMediaQuery = null;
84
+ static _autoMediaListener = null;
85
+ static _autoInstances = new Set();
86
+ static _globalAutoTheme = false;
87
+ static _globalAutoCustomColors = null;
83
88
 
84
89
  /**
85
90
  * Constructor - Always returns an array of instances
@@ -156,6 +161,10 @@ class OverType {
156
161
  this._buildFromScratch();
157
162
  }
158
163
 
164
+ if (this.instanceTheme === 'auto') {
165
+ this.setTheme('auto');
166
+ }
167
+
159
168
  // Setup shortcuts manager
160
169
  this.shortcuts = new ShortcutsManager(this);
161
170
 
@@ -226,7 +235,8 @@ class OverType {
226
235
  toolbarButtons: null, // Defaults to defaultToolbarButtons if toolbar: true
227
236
  statsFormatter: null,
228
237
  smartLists: true, // Enable smart list continuation
229
- codeHighlighter: null // Per-instance code highlighter
238
+ codeHighlighter: null, // Per-instance code highlighter
239
+ spellcheck: false // Browser spellcheck (disabled by default)
230
240
  };
231
241
 
232
242
  // Remove theme and colors from options - these are now global
@@ -414,9 +424,16 @@ class OverType {
414
424
  this.preview.className = 'overtype-preview';
415
425
  this.preview.setAttribute('aria-hidden', 'true');
416
426
 
427
+ // Create placeholder shim
428
+ this.placeholderEl = document.createElement('div');
429
+ this.placeholderEl.className = 'overtype-placeholder';
430
+ this.placeholderEl.setAttribute('aria-hidden', 'true');
431
+ this.placeholderEl.textContent = this.options.placeholder;
432
+
417
433
  // Assemble DOM
418
434
  this.wrapper.appendChild(this.textarea);
419
435
  this.wrapper.appendChild(this.preview);
436
+ this.wrapper.appendChild(this.placeholderEl);
420
437
 
421
438
  // No need to prevent link clicks - pointer-events handles this
422
439
 
@@ -451,7 +468,7 @@ class OverType {
451
468
  this.textarea.setAttribute('autocomplete', 'off');
452
469
  this.textarea.setAttribute('autocorrect', 'off');
453
470
  this.textarea.setAttribute('autocapitalize', 'off');
454
- this.textarea.setAttribute('spellcheck', 'false');
471
+ this.textarea.setAttribute('spellcheck', String(this.options.spellcheck));
455
472
  this.textarea.setAttribute('data-gramm', 'false');
456
473
  this.textarea.setAttribute('data-gramm_editor', 'false');
457
474
  this.textarea.setAttribute('data-enable-grammarly', 'false');
@@ -462,12 +479,22 @@ class OverType {
462
479
  * @private
463
480
  */
464
481
  _createToolbar() {
465
- // Use provided toolbarButtons or default to defaultToolbarButtons
466
- const toolbarButtons = this.options.toolbarButtons || defaultToolbarButtons;
482
+ let toolbarButtons = this.options.toolbarButtons || defaultToolbarButtons;
483
+
484
+ if (this.options.fileUpload?.enabled && !toolbarButtons.some(b => b?.name === 'upload')) {
485
+ const viewModeIdx = toolbarButtons.findIndex(b => b?.name === 'viewMode');
486
+ if (viewModeIdx !== -1) {
487
+ toolbarButtons = [...toolbarButtons];
488
+ toolbarButtons.splice(viewModeIdx, 0, builtinToolbarButtons.separator, builtinToolbarButtons.upload);
489
+ } else {
490
+ toolbarButtons = [...toolbarButtons, builtinToolbarButtons.separator, builtinToolbarButtons.upload];
491
+ }
492
+ }
467
493
 
468
494
  this.toolbar = new Toolbar(this, { toolbarButtons });
469
495
  this.toolbar.create();
470
496
 
497
+
471
498
  // Store listener references for cleanup
472
499
  this._toolbarSelectionListener = () => {
473
500
  if (this.toolbar) {
@@ -513,6 +540,11 @@ class OverType {
513
540
  if (this.options.toolbarButtons) {
514
541
  Object.assign(this.actionsById, buildActionsMap(this.options.toolbarButtons));
515
542
  }
543
+
544
+ // Register upload action when file upload is enabled
545
+ if (this.options.fileUpload?.enabled) {
546
+ Object.assign(this.actionsById, buildActionsMap([builtinToolbarButtons.upload]));
547
+ }
516
548
  }
517
549
 
518
550
  /**
@@ -529,6 +561,8 @@ class OverType {
529
561
  if (this.options.autoResize) {
530
562
  if (!this.container.classList.contains('overtype-auto-resize')) {
531
563
  this._setupAutoResize();
564
+ } else {
565
+ this._updateAutoHeight();
532
566
  }
533
567
  } else {
534
568
  // Ensure auto-resize class is removed
@@ -546,10 +580,135 @@ class OverType {
546
580
  this.toolbar = null;
547
581
  }
548
582
 
583
+ // Update placeholder text
584
+ if (this.placeholderEl) {
585
+ this.placeholderEl.textContent = this.options.placeholder;
586
+ }
587
+
588
+ // Setup or remove file upload
589
+ if (this.options.fileUpload && !this.fileUploadInitialized) {
590
+ this._initFileUpload();
591
+ } else if (!this.options.fileUpload && this.fileUploadInitialized) {
592
+ this._destroyFileUpload();
593
+ }
594
+
549
595
  // Update preview with initial content
550
596
  this.updatePreview();
551
597
  }
552
598
 
599
+ _initFileUpload() {
600
+ const options = this.options.fileUpload;
601
+ if (!options || !options.enabled) return;
602
+
603
+ options.maxSize = options.maxSize || 10 * 1024 * 1024;
604
+ options.mimeTypes = options.mimeTypes || [];
605
+ options.batch = options.batch || false;
606
+ if (!options.onInsertFile || typeof options.onInsertFile !== 'function') {
607
+ console.warn('OverType: fileUpload.onInsertFile callback is required for file uploads.');
608
+ return;
609
+ }
610
+
611
+ this._fileUploadCounter = 0;
612
+ this._boundHandleFilePaste = this._handleFilePaste.bind(this);
613
+ this._boundHandleFileDrop = this._handleFileDrop.bind(this);
614
+ this._boundHandleDragOver = this._handleDragOver.bind(this);
615
+
616
+ this.textarea.addEventListener('paste', this._boundHandleFilePaste);
617
+ this.textarea.addEventListener('drop', this._boundHandleFileDrop);
618
+ this.textarea.addEventListener('dragover', this._boundHandleDragOver);
619
+
620
+ this.fileUploadInitialized = true;
621
+ }
622
+
623
+ _handleFilePaste(e) {
624
+ if (!e?.clipboardData?.files?.length) return;
625
+ e.preventDefault();
626
+ this._handleDataTransfer(e.clipboardData);
627
+ }
628
+
629
+ _handleFileDrop(e) {
630
+ if (!e?.dataTransfer?.files?.length) return;
631
+ e.preventDefault();
632
+ this._handleDataTransfer(e.dataTransfer);
633
+ }
634
+
635
+ _handleDataTransfer(dataTransfer) {
636
+ const files = [];
637
+ for (const file of dataTransfer.files) {
638
+ if (file.size > this.options.fileUpload.maxSize) continue;
639
+ if (this.options.fileUpload.mimeTypes.length > 0
640
+ && !this.options.fileUpload.mimeTypes.includes(file.type)) continue;
641
+
642
+ const id = ++this._fileUploadCounter;
643
+ const prefix = file.type.startsWith('image/') ? '!' : '';
644
+ const placeholder = `${prefix}[Uploading ${file.name} (#${id})...]()`;
645
+ this.insertAtCursor(`${placeholder}\n`);
646
+
647
+ if (this.options.fileUpload.batch) {
648
+ files.push({ file, placeholder });
649
+ continue;
650
+ }
651
+
652
+ this.options.fileUpload.onInsertFile(file).then((text) => {
653
+ this.textarea.value = this.textarea.value.replace(placeholder, text);
654
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
655
+ }, (error) => {
656
+ console.error('OverType: File upload failed', error);
657
+ this.textarea.value = this.textarea.value.replace(placeholder, '[Upload failed]()');
658
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
659
+ });
660
+ }
661
+
662
+ if (this.options.fileUpload.batch && files.length > 0) {
663
+ this.options.fileUpload.onInsertFile(files.map(f => f.file)).then((result) => {
664
+ const texts = Array.isArray(result) ? result : [result];
665
+ texts.forEach((text, index) => {
666
+ this.textarea.value = this.textarea.value.replace(files[index].placeholder, text);
667
+ });
668
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
669
+ }, (error) => {
670
+ console.error('OverType: File upload failed', error);
671
+ files.forEach(({ placeholder }) => {
672
+ this.textarea.value = this.textarea.value.replace(placeholder, '[Upload failed]()');
673
+ });
674
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
675
+ });
676
+ }
677
+ }
678
+
679
+ _handleDragOver(e) {
680
+ e.preventDefault();
681
+ }
682
+
683
+ _destroyFileUpload() {
684
+ this.textarea.removeEventListener('paste', this._boundHandleFilePaste);
685
+ this.textarea.removeEventListener('drop', this._boundHandleFileDrop);
686
+ this.textarea.removeEventListener('dragover', this._boundHandleDragOver);
687
+ this._boundHandleFilePaste = null;
688
+ this._boundHandleFileDrop = null;
689
+ this._boundHandleDragOver = null;
690
+ this.fileUploadInitialized = false;
691
+ }
692
+
693
+ insertAtCursor(text) {
694
+ const start = this.textarea.selectionStart;
695
+ const end = this.textarea.selectionEnd;
696
+
697
+ let inserted = false;
698
+ try {
699
+ inserted = document.execCommand('insertText', false, text);
700
+ } catch (_) {}
701
+
702
+ if (!inserted) {
703
+ const before = this.textarea.value.slice(0, start);
704
+ const after = this.textarea.value.slice(end);
705
+ this.textarea.value = before + text + after;
706
+ this.textarea.setSelectionRange(start + text.length, start + text.length);
707
+ }
708
+
709
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
710
+ }
711
+
553
712
  /**
554
713
  * Update preview with parsed markdown
555
714
  */
@@ -563,7 +722,12 @@ class OverType {
563
722
 
564
723
  // Parse markdown
565
724
  const html = MarkdownParser.parse(text, activeLine, this.options.showActiveLineRaw, this.options.codeHighlighter, isPreviewMode);
566
- this.preview.innerHTML = html || '<span style="color: #808080;">Start typing...</span>';
725
+ this.preview.innerHTML = html;
726
+
727
+ // Show/hide placeholder shim
728
+ if (this.placeholderEl) {
729
+ this.placeholderEl.style.display = text ? 'none' : '';
730
+ }
567
731
 
568
732
  // Apply code block backgrounds
569
733
  this._applyCodeBlockBackgrounds();
@@ -1012,38 +1176,73 @@ class OverType {
1012
1176
  this._createToolbar();
1013
1177
  }
1014
1178
 
1179
+ if (this.fileUploadInitialized) {
1180
+ this._destroyFileUpload();
1181
+ }
1182
+ if (this.options.fileUpload) {
1183
+ this._initFileUpload();
1184
+ }
1185
+
1015
1186
  this._applyOptions();
1016
1187
  this.updatePreview();
1017
1188
  }
1018
1189
 
1190
+ showToolbar() {
1191
+ if (this.toolbar) {
1192
+ this.toolbar.show();
1193
+ } else {
1194
+ this._createToolbar();
1195
+ }
1196
+ }
1197
+
1198
+ hideToolbar() {
1199
+ if (this.toolbar) {
1200
+ this.toolbar.hide();
1201
+ }
1202
+ }
1203
+
1019
1204
  /**
1020
1205
  * Set theme for this instance
1021
1206
  * @param {string|Object} theme - Theme name or custom theme object
1022
1207
  * @returns {this} Returns this for chaining
1023
1208
  */
1024
1209
  setTheme(theme) {
1025
- // Update instance theme
1210
+ OverType._autoInstances.delete(this);
1026
1211
  this.instanceTheme = theme;
1027
1212
 
1028
- // Get theme object
1029
- const themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
1030
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1213
+ if (theme === 'auto') {
1214
+ OverType._autoInstances.add(this);
1215
+ OverType._startAutoListener();
1216
+ this._applyResolvedTheme(resolveAutoTheme('auto'));
1217
+ } else {
1218
+ const themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
1219
+ const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1031
1220
 
1032
- // Update container theme attribute
1033
- if (themeName) {
1034
- this.container.setAttribute('data-theme', themeName);
1221
+ if (themeName) {
1222
+ this.container.setAttribute('data-theme', themeName);
1223
+ }
1224
+
1225
+ if (themeObj && themeObj.colors) {
1226
+ const cssVars = themeToCSSVars(themeObj.colors, themeObj.previewColors);
1227
+ this.container.style.cssText += cssVars;
1228
+ }
1229
+
1230
+ this.updatePreview();
1035
1231
  }
1036
1232
 
1037
- // Apply CSS variables to container for instance override
1233
+ OverType._stopAutoListener();
1234
+ return this;
1235
+ }
1236
+
1237
+ _applyResolvedTheme(themeName) {
1238
+ const themeObj = getTheme(themeName);
1239
+ this.container.setAttribute('data-theme', themeName);
1240
+
1038
1241
  if (themeObj && themeObj.colors) {
1039
- const cssVars = themeToCSSVars(themeObj.colors);
1040
- this.container.style.cssText += cssVars;
1242
+ this.container.style.cssText = themeToCSSVars(themeObj.colors, themeObj.previewColors);
1041
1243
  }
1042
1244
 
1043
- // Update preview to reflect new theme
1044
1245
  this.updatePreview();
1045
-
1046
- return this;
1047
1246
  }
1048
1247
 
1049
1248
  /**
@@ -1251,6 +1450,13 @@ class OverType {
1251
1450
  * Destroy the editor instance
1252
1451
  */
1253
1452
  destroy() {
1453
+ OverType._autoInstances.delete(this);
1454
+ OverType._stopAutoListener();
1455
+
1456
+ if (this.fileUploadInitialized) {
1457
+ this._destroyFileUpload();
1458
+ }
1459
+
1254
1460
  // Remove instance reference
1255
1461
  this.element.overTypeInstance = null;
1256
1462
  OverType.instances.delete(this.element);
@@ -1307,7 +1513,7 @@ class OverType {
1307
1513
  }
1308
1514
  }
1309
1515
 
1310
- return new OverType(el, options);
1516
+ return new OverType(el, options)[0];
1311
1517
  });
1312
1518
  }
1313
1519
 
@@ -1375,59 +1581,89 @@ class OverType {
1375
1581
  * @param {Object} customColors - Optional color overrides
1376
1582
  */
1377
1583
  static setTheme(theme, customColors = null) {
1378
- // Process theme
1584
+ OverType._globalAutoTheme = false;
1585
+ OverType._globalAutoCustomColors = null;
1586
+
1587
+ if (theme === 'auto') {
1588
+ OverType._globalAutoTheme = true;
1589
+ OverType._globalAutoCustomColors = customColors;
1590
+ OverType._startAutoListener();
1591
+ OverType._applyGlobalTheme(resolveAutoTheme('auto'), customColors);
1592
+ return;
1593
+ }
1594
+
1595
+ OverType._stopAutoListener();
1596
+ OverType._applyGlobalTheme(theme, customColors);
1597
+ }
1598
+
1599
+ static _applyGlobalTheme(theme, customColors = null) {
1379
1600
  let themeObj = typeof theme === 'string' ? getTheme(theme) : theme;
1380
1601
 
1381
- // Apply custom colors if provided
1382
1602
  if (customColors) {
1383
1603
  themeObj = mergeTheme(themeObj, customColors);
1384
1604
  }
1385
1605
 
1386
- // Store as current theme
1387
1606
  OverType.currentTheme = themeObj;
1388
-
1389
- // Re-inject styles with new theme
1390
1607
  OverType.injectStyles(true);
1391
1608
 
1392
- // Update all existing instances - update container theme attribute
1609
+ const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1610
+
1393
1611
  document.querySelectorAll('.overtype-container').forEach(container => {
1394
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1395
1612
  if (themeName) {
1396
1613
  container.setAttribute('data-theme', themeName);
1397
1614
  }
1398
1615
  });
1399
1616
 
1400
- // Also handle any old-style wrappers without containers
1401
1617
  document.querySelectorAll('.overtype-wrapper').forEach(wrapper => {
1402
1618
  if (!wrapper.closest('.overtype-container')) {
1403
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1404
1619
  if (themeName) {
1405
1620
  wrapper.setAttribute('data-theme', themeName);
1406
1621
  }
1407
1622
  }
1408
1623
 
1409
- // Trigger preview update for the instance
1410
1624
  const instance = wrapper._instance;
1411
1625
  if (instance) {
1412
1626
  instance.updatePreview();
1413
1627
  }
1414
1628
  });
1415
1629
 
1416
- // Update web components (shadow DOM instances)
1417
- const themeName = typeof themeObj === 'string' ? themeObj : themeObj.name;
1418
1630
  document.querySelectorAll('overtype-editor').forEach(webComponent => {
1419
- // Set the theme attribute to update the theme name
1420
1631
  if (themeName && typeof webComponent.setAttribute === 'function') {
1421
1632
  webComponent.setAttribute('theme', themeName);
1422
1633
  }
1423
- // Also call refreshTheme() to handle cases where the theme name stays the same
1424
- // but the theme object's properties have changed
1425
1634
  if (typeof webComponent.refreshTheme === 'function') {
1426
1635
  webComponent.refreshTheme();
1427
1636
  }
1428
1637
  });
1429
1638
  }
1430
1639
 
1640
+ static _startAutoListener() {
1641
+ if (OverType._autoMediaQuery) return;
1642
+ if (!window.matchMedia) return;
1643
+
1644
+ OverType._autoMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1645
+ OverType._autoMediaListener = (e) => {
1646
+ const resolved = e.matches ? 'cave' : 'solar';
1647
+
1648
+ if (OverType._globalAutoTheme) {
1649
+ OverType._applyGlobalTheme(resolved, OverType._globalAutoCustomColors);
1650
+ }
1651
+
1652
+ OverType._autoInstances.forEach(inst => inst._applyResolvedTheme(resolved));
1653
+ };
1654
+
1655
+ OverType._autoMediaQuery.addEventListener('change', OverType._autoMediaListener);
1656
+ }
1657
+
1658
+ static _stopAutoListener() {
1659
+ if (OverType._autoInstances.size > 0 || OverType._globalAutoTheme) return;
1660
+ if (!OverType._autoMediaQuery) return;
1661
+
1662
+ OverType._autoMediaQuery.removeEventListener('change', OverType._autoMediaListener);
1663
+ OverType._autoMediaQuery = null;
1664
+ OverType._autoMediaListener = null;
1665
+ }
1666
+
1431
1667
  /**
1432
1668
  * Set global code highlighter for all OverType instances
1433
1669
  * @param {Function|null} highlighter - Function that takes (code, language) and returns highlighted HTML
package/src/parser.js CHANGED
@@ -87,6 +87,7 @@ export class MarkdownParser {
87
87
  static parseHeader(html) {
88
88
  return html.replace(/^(#{1,3})\s(.+)$/, (match, hashes, content) => {
89
89
  const level = hashes.length;
90
+ content = this.parseInlineElements(content);
90
91
  return `<h${level}><span class="syntax-marker">${hashes} </span>${content}</h${level}>`;
91
92
  });
92
93
  }
@@ -121,6 +122,7 @@ export class MarkdownParser {
121
122
  */
122
123
  static parseBulletList(html) {
123
124
  return html.replace(/^((?:&nbsp;)*)([-*+])\s(.+)$/, (match, indent, marker, content) => {
125
+ content = this.parseInlineElements(content);
124
126
  return `${indent}<li class="bullet-list"><span class="syntax-marker">${marker} </span>${content}</li>`;
125
127
  });
126
128
  }
@@ -133,6 +135,7 @@ export class MarkdownParser {
133
135
  */
134
136
  static parseTaskList(html, isPreviewMode = false) {
135
137
  return html.replace(/^((?:&nbsp;)*)-\s+\[([ xX])\]\s+(.+)$/, (match, indent, checked, content) => {
138
+ content = this.parseInlineElements(content);
136
139
  if (isPreviewMode) {
137
140
  // Preview mode: render actual checkbox
138
141
  const isChecked = checked.toLowerCase() === 'x';
@@ -151,6 +154,7 @@ export class MarkdownParser {
151
154
  */
152
155
  static parseNumberedList(html) {
153
156
  return html.replace(/^((?:&nbsp;)*)(\d+\.)\s(.+)$/, (match, indent, marker, content) => {
157
+ content = this.parseInlineElements(content);
154
158
  return `${indent}<li class="ordered-list"><span class="syntax-marker">${marker} </span>${content}</li>`;
155
159
  });
156
160
  }
@@ -188,7 +192,7 @@ export class MarkdownParser {
188
192
  */
189
193
  static parseItalic(html) {
190
194
  // Single asterisk - must not be adjacent to other asterisks
191
- // Also must not be inside a syntax-marker span (to avoid matching bullet list markers)
195
+ // Must not be inside a syntax-marker span (avoid matching bullet list markers like ">* ")
192
196
  html = html.replace(/(?<![\*>])\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em><span class="syntax-marker">*</span>$1<span class="syntax-marker">*</span></em>');
193
197
 
194
198
  // Single underscore - must be at word boundaries to avoid matching inside words
@@ -464,8 +468,10 @@ export class MarkdownParser {
464
468
  html = this.parseBulletList(html);
465
469
  html = this.parseNumberedList(html);
466
470
 
467
- // Parse inline elements
468
- html = this.parseInlineElements(html);
471
+ // Parse inline elements (skip for headers and list items — already parsed inside those functions)
472
+ if (!html.includes('<li') && !html.includes('<h')) {
473
+ html = this.parseInlineElements(html);
474
+ }
469
475
 
470
476
  // Wrap in div to maintain line structure
471
477
  if (html.trim() === '') {