overtype 2.3.10 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtype",
3
- "version": "2.3.10",
3
+ "version": "2.4.0",
4
4
  "description": "A lightweight markdown editor library with perfect WYSIWYG alignment using an invisible textarea overlay",
5
5
  "main": "dist/overtype.cjs",
6
6
  "module": "dist/overtype.esm.js",
@@ -13,7 +13,7 @@
13
13
  "types": "./dist/overtype.d.ts",
14
14
  "import": "./dist/overtype.esm.js",
15
15
  "require": "./dist/overtype.cjs",
16
- "browser": "./dist/overtype.iife.min.js"
16
+ "browser": "./dist/overtype.min.js"
17
17
  },
18
18
  "./parser": {
19
19
  "import": "./src/parser.js",
@@ -31,7 +31,7 @@
31
31
  "build:prod": "npm test && npm run build",
32
32
  "dev": "http-server website -p 8080 -c-1",
33
33
  "watch": "node scripts/build.js --watch",
34
- "test": "node test/overtype.test.js && node test/preview-mode.test.js && node test/links.test.js && node test/api-methods.test.js && node test/comprehensive-alignment.test.js && node test/sanctuary-parsing.test.js && node test/mode-switching.test.js && node test/syntax-highlighting.test.js && node test/webcomponent.test.js && node test/custom-syntax.test.js && node test/auto-theme.test.js && npm run test:types",
34
+ "test": "node test/overtype.test.js && node test/preview-mode.test.js && node test/links.test.js && node test/api-methods.test.js && node test/keyboard-accessibility.test.js && node test/comprehensive-alignment.test.js && node test/sanctuary-parsing.test.js && node test/mode-switching.test.js && node test/toolbar.test.js && node test/syntax-highlighting.test.js && node test/webcomponent.test.js && node test/custom-syntax.test.js && node test/auto-theme.test.js && npm run test:types",
35
35
  "test:main": "node test/overtype.test.js",
36
36
  "test:preview": "node test/preview-mode.test.js",
37
37
  "test:links": "node test/links.test.js",
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { computePosition, offset, shift, flip } from '@floating-ui/dom';
7
+ import { MarkdownParser } from './parser.js';
7
8
 
8
9
  export class LinkTooltip {
9
10
  constructor(editor) {
@@ -85,7 +86,10 @@ export class LinkTooltip {
85
86
  e.preventDefault();
86
87
  e.stopPropagation();
87
88
  if (this.currentLink) {
88
- window.open(this.currentLink.url, '_blank');
89
+ const safeUrl = MarkdownParser.sanitizeUrl(this.currentLink.url);
90
+ if (safeUrl !== '#') {
91
+ window.open(safeUrl, '_blank');
92
+ }
89
93
  this.hide();
90
94
  }
91
95
  });
@@ -123,7 +127,7 @@ export class LinkTooltip {
123
127
  if (position >= start && position <= end) {
124
128
  return {
125
129
  text: match[1],
126
- url: match[2],
130
+ url: this.transformUrl(match[2]),
127
131
  index: linkIndex,
128
132
  start: start,
129
133
  end: end
@@ -135,6 +139,18 @@ export class LinkTooltip {
135
139
  return null;
136
140
  }
137
141
 
142
+ transformUrl(url) {
143
+ const transform = this.editor.options.transformLinkUrl;
144
+ if (typeof transform !== 'function') return url;
145
+ try {
146
+ const result = transform(url);
147
+ return typeof result === 'string' ? result : url;
148
+ } catch (e) {
149
+ console.warn('transformLinkUrl threw:', e);
150
+ return url;
151
+ }
152
+ }
153
+
138
154
  async show(linkInfo) {
139
155
  this.currentLink = linkInfo;
140
156
  this.cancelHide();
package/src/overtype.d.ts CHANGED
@@ -84,6 +84,15 @@ export interface ToolbarButton {
84
84
  setValue: (value: string) => void;
85
85
  event: MouseEvent;
86
86
  }) => void | Promise<void>;
87
+
88
+ /** Canonical action identifier used by shortcuts and performAction */
89
+ actionId?: string;
90
+
91
+ /** Return true when this button should be announced as pressed */
92
+ isActive?: (context: {
93
+ editor: OverType;
94
+ activeFormats: string[];
95
+ }) => boolean;
87
96
  }
88
97
 
89
98
  export interface MobileOptions {
@@ -122,6 +131,7 @@ export interface Options {
122
131
  spellcheck?: boolean; // Browser spellcheck (default: false)
123
132
  statsFormatter?: (stats: Stats) => string;
124
133
  codeHighlighter?: ((code: string, language: string) => string) | null; // Per-instance code highlighter
134
+ transformLinkUrl?: ((url: string) => string) | null; // Transform URLs shown/opened in the link tooltip
125
135
 
126
136
  // Theme (deprecated in favor of global theme)
127
137
  theme?: string | Theme;
@@ -208,6 +218,8 @@ export interface OverTypeInstance {
208
218
  showToolbar(): void;
209
219
  hideToolbar(): void;
210
220
  insertAtCursor(text: string): void;
221
+ indentSelection(): void;
222
+ outdentSelection(): void;
211
223
 
212
224
  // HTML output methods
213
225
  getRenderedHTML(options?: RenderOptions): string;
package/src/overtype.js CHANGED
@@ -12,6 +12,25 @@ import { Toolbar } from './toolbar.js';
12
12
  import { LinkTooltip } from './link-tooltip.js';
13
13
  import { defaultToolbarButtons, toolbarButtons as builtinToolbarButtons } from './toolbar-buttons.js';
14
14
 
15
+ let _isSafariCache;
16
+ /**
17
+ * Detect Safari (desktop, iOS, and iPadOS), excluding Chromium/Firefox-on-iOS.
18
+ * Memoized; guards against non-browser environments.
19
+ * @returns {boolean}
20
+ */
21
+ function isSafariBrowser() {
22
+ if (_isSafariCache !== undefined) return _isSafariCache;
23
+ _isSafariCache = false;
24
+ if (typeof navigator !== 'undefined') {
25
+ const ua = navigator.userAgent || '';
26
+ _isSafariCache =
27
+ /^((?!chrome|android|crios|fxios|edg|opr).)*safari/i.test(ua) ||
28
+ /iPad|iPhone|iPod/.test(ua) ||
29
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 && /Safari/.test(ua));
30
+ }
31
+ return _isSafariCache;
32
+ }
33
+
15
34
  /**
16
35
  * Build action map from toolbar button configurations
17
36
  * @param {Array} buttons - Array of button config objects
@@ -131,6 +150,8 @@ class OverType {
131
150
  this.options = this._mergeOptions(options);
132
151
  this.instanceId = ++OverType.instanceCount;
133
152
  this.initialized = false;
153
+ this._isSafari = isSafariBrowser();
154
+ this._safariReflowRaf = null;
134
155
 
135
156
  // Inject styles if needed
136
157
  OverType.injectStyles();
@@ -225,7 +246,8 @@ class OverType {
225
246
  statsFormatter: null,
226
247
  smartLists: true, // Enable smart list continuation
227
248
  codeHighlighter: null, // Per-instance code highlighter
228
- spellcheck: false // Browser spellcheck (disabled by default)
249
+ spellcheck: false, // Browser spellcheck (disabled by default)
250
+ transformLinkUrl: null // Transform URLs shown/opened in the link tooltip
229
251
  };
230
252
 
231
253
  // Remove theme and colors from options - these are now global
@@ -296,6 +318,9 @@ class OverType {
296
318
 
297
319
  // Disable autofill, spellcheck, and extensions
298
320
  this._configureTextarea();
321
+ this._ensureTextareaId();
322
+
323
+ this._syncPreviewInteractivity();
299
324
 
300
325
  // Apply any new options
301
326
  this._applyOptions();
@@ -390,6 +415,8 @@ class OverType {
390
415
  });
391
416
  }
392
417
 
418
+ this._ensureTextareaId();
419
+
393
420
  // Create preview div
394
421
  this.preview = document.createElement('div');
395
422
  this.preview.className = 'overtype-preview';
@@ -429,6 +456,8 @@ class OverType {
429
456
  // Ensure auto-resize class is removed if not using auto-resize
430
457
  this.container.classList.remove('overtype-auto-resize');
431
458
  }
459
+
460
+ this._syncPreviewInteractivity();
432
461
  }
433
462
 
434
463
  /**
@@ -445,6 +474,35 @@ class OverType {
445
474
  this.textarea.setAttribute('data-enable-grammarly', 'false');
446
475
  }
447
476
 
477
+ /**
478
+ * Ensure the textarea can be referenced by aria-controls
479
+ * @private
480
+ */
481
+ _ensureTextareaId() {
482
+ if (!this.textarea.id) {
483
+ this.textarea.id = `overtype-${this.instanceId}-input`;
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Keep rendered preview content out of keyboard navigation until Preview mode.
489
+ * @private
490
+ */
491
+ _syncPreviewInteractivity() {
492
+ if (!this.preview || !this.container) return;
493
+
494
+ const isPreviewMode = this.container.dataset.mode === 'preview';
495
+ this.preview.inert = !isPreviewMode;
496
+ this.preview.toggleAttribute('inert', !isPreviewMode);
497
+
498
+ if (isPreviewMode) {
499
+ this.preview.removeAttribute('aria-hidden');
500
+ return;
501
+ }
502
+
503
+ this.preview.setAttribute('aria-hidden', 'true');
504
+ }
505
+
448
506
  /**
449
507
  * Create and setup toolbar
450
508
  * @private
@@ -850,6 +908,28 @@ class OverType {
850
908
  handleInput(event) {
851
909
  this.updatePreview();
852
910
  this._notifyChange();
911
+ this._scheduleSafariReflow();
912
+ }
913
+
914
+ /**
915
+ * Force Safari to re-shape stale textarea text after an edit.
916
+ * Safari can leave a textarea's glyph layout cached after incremental edits,
917
+ * desyncing the caret/wrap from the styled preview overlay. Toggling
918
+ * letter-spacing (with !important to beat the stylesheet rule) and reading
919
+ * offsetHeight forces a synchronous re-shape. Safari-only, coalesced to one
920
+ * run per animation frame.
921
+ * @private
922
+ */
923
+ _scheduleSafariReflow() {
924
+ if (!this._isSafari || this._safariReflowRaf) return;
925
+ this._safariReflowRaf = requestAnimationFrame(() => {
926
+ this._safariReflowRaf = null;
927
+ const ta = this.textarea;
928
+ if (!ta) return;
929
+ ta.style.setProperty('letter-spacing', '-0.001px', 'important');
930
+ void ta.offsetHeight;
931
+ ta.style.removeProperty('letter-spacing');
932
+ });
853
933
  }
854
934
 
855
935
  /**
@@ -877,75 +957,16 @@ class OverType {
877
957
  * @private
878
958
  */
879
959
  handleKeydown(event) {
880
- // Handle Tab key to prevent focus loss and insert spaces
960
+ // Let collapsed Tab/Shift+Tab use native focus traversal.
881
961
  if (event.key === 'Tab') {
882
962
  const start = this.textarea.selectionStart;
883
963
  const end = this.textarea.selectionEnd;
884
- const value = this.textarea.value;
885
964
 
886
- // If Shift+Tab without a selection, allow default behavior (navigate to previous element)
887
- if (event.shiftKey && start === end) {
965
+ if (start !== end && this._canEditTextarea()) {
966
+ event.preventDefault();
967
+ event.shiftKey ? this.outdentSelection() : this.indentSelection();
888
968
  return;
889
969
  }
890
-
891
- event.preventDefault();
892
-
893
- // If there's a selection, indent/outdent based on shift key
894
- if (start !== end && event.shiftKey) {
895
- // Outdent: remove 2 spaces from start of each selected line
896
- const before = value.substring(0, start);
897
- const selection = value.substring(start, end);
898
- const after = value.substring(end);
899
-
900
- const lines = selection.split('\n');
901
- const outdented = lines.map(line => line.replace(/^ /, '')).join('\n');
902
-
903
- // Try to use execCommand first to preserve undo history
904
- if (document.execCommand) {
905
- // Select the text that needs to be replaced
906
- this.textarea.setSelectionRange(start, end);
907
- document.execCommand('insertText', false, outdented);
908
- } else {
909
- // Fallback to direct manipulation
910
- this.textarea.value = before + outdented + after;
911
- this.textarea.selectionStart = start;
912
- this.textarea.selectionEnd = start + outdented.length;
913
- }
914
- } else if (start !== end) {
915
- // Indent: add 2 spaces to start of each selected line
916
- const before = value.substring(0, start);
917
- const selection = value.substring(start, end);
918
- const after = value.substring(end);
919
-
920
- const lines = selection.split('\n');
921
- const indented = lines.map(line => ' ' + line).join('\n');
922
-
923
- // Try to use execCommand first to preserve undo history
924
- if (document.execCommand) {
925
- // Select the text that needs to be replaced
926
- this.textarea.setSelectionRange(start, end);
927
- document.execCommand('insertText', false, indented);
928
- } else {
929
- // Fallback to direct manipulation
930
- this.textarea.value = before + indented + after;
931
- this.textarea.selectionStart = start;
932
- this.textarea.selectionEnd = start + indented.length;
933
- }
934
- } else {
935
- // No selection: just insert 2 spaces
936
- // Use execCommand to preserve undo history
937
- if (document.execCommand) {
938
- document.execCommand('insertText', false, ' ');
939
- } else {
940
- // Fallback to direct manipulation
941
- this.textarea.value = value.substring(0, start) + ' ' + value.substring(end);
942
- this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
943
- }
944
- }
945
-
946
- // Trigger input event to update preview
947
- this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
948
- return;
949
970
  }
950
971
 
951
972
  // Handle Enter key for smart list continuation
@@ -1134,6 +1155,7 @@ class OverType {
1134
1155
 
1135
1156
  if (didChange) {
1136
1157
  this._notifyChange();
1158
+ this._scheduleSafariReflow();
1137
1159
  }
1138
1160
  }
1139
1161
 
@@ -1205,6 +1227,77 @@ class OverType {
1205
1227
  getPreviewHTML() {
1206
1228
  return this.preview.innerHTML;
1207
1229
  }
1230
+
1231
+ /**
1232
+ * Indent the current line or selected lines by two spaces.
1233
+ */
1234
+ indentSelection() {
1235
+ this._replaceSelectedLines(line => ` ${line}`);
1236
+ }
1237
+
1238
+ /**
1239
+ * Outdent the current line or selected lines by up to two spaces or one tab.
1240
+ */
1241
+ outdentSelection() {
1242
+ this._replaceSelectedLines(line => line.replace(/^( {1,2}|\t)/, ''));
1243
+ }
1244
+
1245
+ /**
1246
+ * Replace full lines touched by the current selection.
1247
+ * @private
1248
+ */
1249
+ _replaceSelectedLines(transformLine) {
1250
+ if (!this._canEditTextarea()) return false;
1251
+
1252
+ const textarea = this.textarea;
1253
+ const { selectionStart, selectionEnd, value } = textarea;
1254
+ const lineStart = value.lastIndexOf('\n', selectionStart - 1) + 1;
1255
+ const effectiveEnd = this._effectiveSelectionEnd(value, selectionStart, selectionEnd);
1256
+ const lineEndOffset = value.indexOf('\n', effectiveEnd);
1257
+ const lineEnd = lineEndOffset === -1 ? value.length : lineEndOffset;
1258
+ const selectedLines = value.slice(lineStart, lineEnd);
1259
+ const replacement = selectedLines
1260
+ .split('\n')
1261
+ .map(transformLine)
1262
+ .join('\n');
1263
+
1264
+ if (replacement === selectedLines) return false;
1265
+
1266
+ // Replace via execCommand to preserve native undo history (matches
1267
+ // insertAtCursor); fall back to setRangeText where execCommand is unavailable.
1268
+ textarea.setSelectionRange(lineStart, lineEnd);
1269
+ let inserted = false;
1270
+ try {
1271
+ inserted = document.execCommand('insertText', false, replacement);
1272
+ } catch (_) {}
1273
+
1274
+ if (!inserted) {
1275
+ textarea.setRangeText(replacement, lineStart, lineEnd, 'preserve');
1276
+ }
1277
+
1278
+ textarea.setSelectionRange(lineStart, lineStart + replacement.length);
1279
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
1280
+
1281
+ return true;
1282
+ }
1283
+
1284
+ /**
1285
+ * @private
1286
+ */
1287
+ _effectiveSelectionEnd(value, selectionStart, selectionEnd) {
1288
+ if (selectionEnd > selectionStart && value[selectionEnd - 1] === '\n') {
1289
+ return selectionEnd - 1;
1290
+ }
1291
+
1292
+ return selectionEnd;
1293
+ }
1294
+
1295
+ /**
1296
+ * @private
1297
+ */
1298
+ _canEditTextarea() {
1299
+ return this.textarea && !this.textarea.disabled && !this.textarea.readOnly;
1300
+ }
1208
1301
 
1209
1302
  /**
1210
1303
  * Get clean HTML without any OverType-specific markup
@@ -1495,6 +1588,7 @@ class OverType {
1495
1588
  */
1496
1589
  showNormalEditMode() {
1497
1590
  this.container.dataset.mode = 'normal';
1591
+ this._syncPreviewInteractivity();
1498
1592
  this.updatePreview(); // Re-render with normal mode (e.g., show syntax markers)
1499
1593
  this._updateAutoHeight();
1500
1594
 
@@ -1513,6 +1607,7 @@ class OverType {
1513
1607
  */
1514
1608
  showPlainTextarea() {
1515
1609
  this.container.dataset.mode = 'plain';
1610
+ this._syncPreviewInteractivity();
1516
1611
  this._updateAutoHeight();
1517
1612
 
1518
1613
  // Update toolbar button if exists
@@ -1533,6 +1628,7 @@ class OverType {
1533
1628
  */
1534
1629
  showPreviewMode() {
1535
1630
  this.container.dataset.mode = 'preview';
1631
+ this._syncPreviewInteractivity();
1536
1632
  this.updatePreview(); // Re-render with preview mode (e.g., checkboxes)
1537
1633
  this._updateAutoHeight();
1538
1634
  return this;
@@ -1558,11 +1654,17 @@ class OverType {
1558
1654
  this.shortcuts.destroy();
1559
1655
  }
1560
1656
 
1657
+ // Cancel any pending Safari reflow nudge
1658
+ if (this._safariReflowRaf) {
1659
+ cancelAnimationFrame(this._safariReflowRaf);
1660
+ this._safariReflowRaf = null;
1661
+ }
1662
+
1561
1663
  // Remove DOM if created by us
1562
1664
  if (this.wrapper) {
1563
1665
  const content = this.getValue();
1564
1666
  this.wrapper.remove();
1565
-
1667
+
1566
1668
  // Restore original content
1567
1669
  this.element.textContent = content;
1568
1670
  }
@@ -1598,11 +1700,19 @@ class OverType {
1598
1700
 
1599
1701
  // Parse data-ot-* attributes (kebab-case to camelCase)
1600
1702
  for (const attr of el.attributes) {
1601
- if (attr.name.startsWith('data-ot-')) {
1602
- const kebab = attr.name.slice(8); // Remove 'data-ot-'
1603
- const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1604
- options[key] = OverType._parseDataValue(attr.value);
1703
+ if (!attr.name.startsWith('data-ot-')) continue;
1704
+ const kebab = attr.name.slice(8); // Remove 'data-ot-'
1705
+
1706
+ // data-ot-textarea-<attr> maps onto textareaProps (e.g. data-ot-textarea-required).
1707
+ // data-ot-textarea-props is the whole-object JSON form, handled generically below.
1708
+ if (kebab.startsWith('textarea-') && kebab !== 'textarea-props') {
1709
+ const propKey = kebab.slice(9).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1710
+ options.textareaProps = { ...(options.textareaProps || {}), [propKey]: OverType._parseDataValue(attr.value) };
1711
+ continue;
1605
1712
  }
1713
+
1714
+ const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1715
+ options[key] = OverType._parseDataValue(attr.value);
1606
1716
  }
1607
1717
 
1608
1718
  return new OverType(el, options)[0];
@@ -1647,6 +1757,14 @@ class OverType {
1647
1757
  if (value === 'false') return false;
1648
1758
  if (value === 'null') return null;
1649
1759
  if (value !== '' && !isNaN(Number(value))) return Number(value);
1760
+ const trimmed = value.trim();
1761
+ if (trimmed[0] === '{' || trimmed[0] === '[') {
1762
+ try {
1763
+ return JSON.parse(trimmed);
1764
+ } catch (e) {
1765
+ return value;
1766
+ }
1767
+ }
1650
1768
  return value;
1651
1769
  }
1652
1770
 
@@ -1855,7 +1973,15 @@ class OverType {
1855
1973
  * Initialize global event listeners
1856
1974
  */
1857
1975
  static initGlobalListeners() {
1858
- if (OverType.globalListenersInitialized) return;
1976
+ const globalScope = typeof window !== 'undefined' ? window : globalThis;
1977
+ const globalListenersKey = '__overtypeGlobalListenersInitialized';
1978
+
1979
+ if (OverType.globalListenersInitialized || globalScope[globalListenersKey]) {
1980
+ OverType.globalListenersInitialized = true;
1981
+ return;
1982
+ }
1983
+
1984
+ globalScope[globalListenersKey] = true;
1859
1985
 
1860
1986
  // Input event
1861
1987
  document.addEventListener('input', (e) => {
package/src/shortcuts.js CHANGED
@@ -22,6 +22,18 @@ export class ShortcutsManager {
22
22
 
23
23
  if (!modKey) return false;
24
24
 
25
+ if (event.key === ']') {
26
+ event.preventDefault();
27
+ this.editor.indentSelection();
28
+ return true;
29
+ }
30
+
31
+ if (event.key === '[') {
32
+ event.preventDefault();
33
+ this.editor.outdentSelection();
34
+ return true;
35
+ }
36
+
25
37
  let actionId = null;
26
38
 
27
39
  switch (event.key.toLowerCase()) {
@@ -19,6 +19,7 @@ export const toolbarButtons = {
19
19
  actionId: 'toggleBold',
20
20
  icon: icons.boldIcon,
21
21
  title: 'Bold (Ctrl+B)',
22
+ isActive: ({ activeFormats }) => activeFormats.includes('bold'),
22
23
  action: ({ editor }) => {
23
24
  markdownActions.toggleBold(editor.textarea);
24
25
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -30,6 +31,7 @@ export const toolbarButtons = {
30
31
  actionId: 'toggleItalic',
31
32
  icon: icons.italicIcon,
32
33
  title: 'Italic (Ctrl+I)',
34
+ isActive: ({ activeFormats }) => activeFormats.includes('italic'),
33
35
  action: ({ editor }) => {
34
36
  markdownActions.toggleItalic(editor.textarea);
35
37
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -41,6 +43,7 @@ export const toolbarButtons = {
41
43
  actionId: 'toggleCode',
42
44
  icon: icons.codeIcon,
43
45
  title: 'Inline Code',
46
+ isActive: () => false,
44
47
  action: ({ editor }) => {
45
48
  markdownActions.toggleCode(editor.textarea);
46
49
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -68,6 +71,7 @@ export const toolbarButtons = {
68
71
  actionId: 'toggleH1',
69
72
  icon: icons.h1Icon,
70
73
  title: 'Heading 1',
74
+ isActive: ({ activeFormats }) => activeFormats.includes('header'),
71
75
  action: ({ editor }) => {
72
76
  markdownActions.toggleH1(editor.textarea);
73
77
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -79,6 +83,7 @@ export const toolbarButtons = {
79
83
  actionId: 'toggleH2',
80
84
  icon: icons.h2Icon,
81
85
  title: 'Heading 2',
86
+ isActive: ({ activeFormats }) => activeFormats.includes('header-2'),
82
87
  action: ({ editor }) => {
83
88
  markdownActions.toggleH2(editor.textarea);
84
89
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -90,6 +95,7 @@ export const toolbarButtons = {
90
95
  actionId: 'toggleH3',
91
96
  icon: icons.h3Icon,
92
97
  title: 'Heading 3',
98
+ isActive: ({ activeFormats }) => activeFormats.includes('header-3'),
93
99
  action: ({ editor }) => {
94
100
  markdownActions.toggleH3(editor.textarea);
95
101
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -101,6 +107,7 @@ export const toolbarButtons = {
101
107
  actionId: 'toggleBulletList',
102
108
  icon: icons.bulletListIcon,
103
109
  title: 'Bullet List',
110
+ isActive: ({ activeFormats }) => activeFormats.includes('bullet-list'),
104
111
  action: ({ editor }) => {
105
112
  markdownActions.toggleBulletList(editor.textarea);
106
113
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -112,6 +119,7 @@ export const toolbarButtons = {
112
119
  actionId: 'toggleNumberedList',
113
120
  icon: icons.orderedListIcon,
114
121
  title: 'Numbered List',
122
+ isActive: ({ activeFormats }) => activeFormats.includes('numbered-list'),
115
123
  action: ({ editor }) => {
116
124
  markdownActions.toggleNumberedList(editor.textarea);
117
125
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
@@ -123,6 +131,7 @@ export const toolbarButtons = {
123
131
  actionId: 'toggleTaskList',
124
132
  icon: icons.taskListIcon,
125
133
  title: 'Task List',
134
+ isActive: ({ activeFormats }) => activeFormats.includes('task-list'),
126
135
  action: ({ editor }) => {
127
136
  if (markdownActions.toggleTaskList) {
128
137
  markdownActions.toggleTaskList(editor.textarea);
@@ -136,6 +145,7 @@ export const toolbarButtons = {
136
145
  actionId: 'toggleQuote',
137
146
  icon: icons.quoteIcon,
138
147
  title: 'Quote',
148
+ isActive: ({ activeFormats }) => activeFormats.includes('quote'),
139
149
  action: ({ editor }) => {
140
150
  markdownActions.toggleQuote(editor.textarea);
141
151
  editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));