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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OverType v2.3.10
2
+ * OverType v2.4.0
3
3
  * A lightweight markdown editor library with perfect WYSIWYG alignment
4
4
  * @license MIT
5
5
  * @author David Miranda
@@ -777,6 +777,16 @@ var OverTypeEditor = (() => {
777
777
  const modKey = isMac ? event.metaKey : event.ctrlKey;
778
778
  if (!modKey)
779
779
  return false;
780
+ if (event.key === "]") {
781
+ event.preventDefault();
782
+ this.editor.indentSelection();
783
+ return true;
784
+ }
785
+ if (event.key === "[") {
786
+ event.preventDefault();
787
+ this.editor.outdentSelection();
788
+ return true;
789
+ }
780
790
  let actionId = null;
781
791
  switch (event.key.toLowerCase()) {
782
792
  case "b":
@@ -2889,6 +2899,10 @@ ${blockSuffix}` : suffix;
2889
2899
  this.editor = editor;
2890
2900
  this.container = null;
2891
2901
  this.buttons = {};
2902
+ this.currentItemIndex = 0;
2903
+ this.handleDocumentClick = null;
2904
+ this.activeDropdown = null;
2905
+ this.activeDropdownButton = null;
2892
2906
  this.toolbarButtons = options.toolbarButtons || [];
2893
2907
  }
2894
2908
  /**
@@ -2897,8 +2911,10 @@ ${blockSuffix}` : suffix;
2897
2911
  create() {
2898
2912
  this.container = document.createElement("div");
2899
2913
  this.container.className = "overtype-toolbar";
2914
+ this.container.id = this.getInstanceElementId("toolbar");
2900
2915
  this.container.setAttribute("role", "toolbar");
2901
2916
  this.container.setAttribute("aria-label", "Formatting toolbar");
2917
+ this.container.setAttribute("aria-controls", this.editor.textarea.id);
2902
2918
  this.toolbarButtons.forEach((buttonConfig) => {
2903
2919
  if (buttonConfig.name === "separator") {
2904
2920
  const separator = this.createSeparator();
@@ -2909,8 +2925,116 @@ ${blockSuffix}` : suffix;
2909
2925
  this.container.appendChild(button);
2910
2926
  }
2911
2927
  });
2928
+ this.setupRovingTabIndex();
2929
+ this.updateButtonStates();
2912
2930
  this.editor.container.insertBefore(this.container, this.editor.wrapper);
2913
2931
  }
2932
+ /**
2933
+ * Build a stable id from the owning OverType instance
2934
+ */
2935
+ getInstanceElementId(name) {
2936
+ return `overtype-${this.editor.instanceId}-${name}`;
2937
+ }
2938
+ /**
2939
+ * Configure toolbar focus management per the ARIA toolbar pattern
2940
+ */
2941
+ setupRovingTabIndex() {
2942
+ const toolbarItems = this.getToolbarItems();
2943
+ if (toolbarItems.length === 0) {
2944
+ return;
2945
+ }
2946
+ this.currentItemIndex = this.getValidItemIndex(this.currentItemIndex);
2947
+ this.updateTabIndexes();
2948
+ this.container.addEventListener("keydown", (e) => {
2949
+ this.onToolbarKeydown(e);
2950
+ });
2951
+ this.container.addEventListener("focusin", (e) => {
2952
+ this.onToolbarFocusin(e);
2953
+ });
2954
+ }
2955
+ /**
2956
+ * Get toolbar buttons in DOM order for keyboard navigation
2957
+ */
2958
+ getToolbarItems() {
2959
+ if (!this.container) {
2960
+ return [];
2961
+ }
2962
+ return Array.from(this.container.querySelectorAll(".overtype-toolbar-button"));
2963
+ }
2964
+ /**
2965
+ * Handle keyboard navigation within the toolbar
2966
+ */
2967
+ onToolbarKeydown(e) {
2968
+ const toolbarItems = this.getToolbarItems();
2969
+ if (!toolbarItems.includes(e.target)) {
2970
+ return;
2971
+ }
2972
+ switch (e.key) {
2973
+ case "ArrowRight":
2974
+ e.preventDefault();
2975
+ this.focusItem(this.currentItemIndex + 1);
2976
+ break;
2977
+ case "ArrowLeft":
2978
+ e.preventDefault();
2979
+ this.focusItem(this.currentItemIndex - 1);
2980
+ break;
2981
+ case "Home":
2982
+ e.preventDefault();
2983
+ this.focusItem(0);
2984
+ break;
2985
+ case "End":
2986
+ e.preventDefault();
2987
+ this.focusItem(toolbarItems.length - 1);
2988
+ break;
2989
+ }
2990
+ }
2991
+ /**
2992
+ * Remember the focused toolbar item as the toolbar tab stop
2993
+ */
2994
+ onToolbarFocusin(e) {
2995
+ const focusedItemIndex = this.getToolbarItems().indexOf(e.target);
2996
+ if (focusedItemIndex === -1) {
2997
+ return;
2998
+ }
2999
+ this.currentItemIndex = focusedItemIndex;
3000
+ this.updateTabIndexes();
3001
+ }
3002
+ /**
3003
+ * Move focus to a toolbar item and make it the only tab stop
3004
+ */
3005
+ focusItem(index) {
3006
+ const toolbarItems = this.getToolbarItems();
3007
+ if (toolbarItems.length === 0) {
3008
+ return;
3009
+ }
3010
+ this.currentItemIndex = this.getValidItemIndex(index, toolbarItems);
3011
+ this.updateTabIndexes();
3012
+ toolbarItems[this.currentItemIndex].focus();
3013
+ }
3014
+ /**
3015
+ * Normalize toolbar item indexes with wrapping
3016
+ */
3017
+ getValidItemIndex(index, toolbarItems = this.getToolbarItems()) {
3018
+ const itemCount = toolbarItems.length;
3019
+ if (itemCount === 0) {
3020
+ return 0;
3021
+ }
3022
+ if (index < 0) {
3023
+ return itemCount - 1;
3024
+ }
3025
+ if (index >= itemCount) {
3026
+ return 0;
3027
+ }
3028
+ return index;
3029
+ }
3030
+ /**
3031
+ * Keep exactly one toolbar item in the page tab sequence
3032
+ */
3033
+ updateTabIndexes() {
3034
+ this.getToolbarItems().forEach((item, index) => {
3035
+ item.tabIndex = index === this.currentItemIndex ? 0 : -1;
3036
+ });
3037
+ }
2914
3038
  /**
2915
3039
  * Create a toolbar separator
2916
3040
  */
@@ -2934,10 +3058,22 @@ ${blockSuffix}` : suffix;
2934
3058
  if (buttonConfig.name === "viewMode") {
2935
3059
  button.classList.add("has-dropdown");
2936
3060
  button.dataset.dropdown = "true";
2937
- button.addEventListener("click", (e) => {
3061
+ button.setAttribute("aria-haspopup", "menu");
3062
+ button.setAttribute("aria-expanded", "false");
3063
+ button._clickHandler = (e) => {
2938
3064
  e.preventDefault();
2939
3065
  this.toggleViewModeDropdown(button);
2940
- });
3066
+ };
3067
+ button._keydownHandler = (e) => {
3068
+ if (!["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
3069
+ return;
3070
+ }
3071
+ e.preventDefault();
3072
+ const placement = e.key === "ArrowUp" ? "last" : "current";
3073
+ this.openViewModeDropdown(button, placement);
3074
+ };
3075
+ button.addEventListener("click", button._clickHandler);
3076
+ button.addEventListener("keydown", button._keydownHandler);
2941
3077
  return button;
2942
3078
  }
2943
3079
  button._clickHandler = (e) => {
@@ -2992,12 +3128,17 @@ ${blockSuffix}` : suffix;
2992
3128
  * Not exposed to users - viewMode button behavior is fixed
2993
3129
  */
2994
3130
  toggleViewModeDropdown(button) {
2995
- const existingDropdown = document.querySelector(".overtype-dropdown-menu");
2996
- if (existingDropdown) {
2997
- existingDropdown.remove();
2998
- button.classList.remove("dropdown-active");
3131
+ if (this.activeDropdown) {
3132
+ this.closeViewModeDropdown(button);
2999
3133
  return;
3000
3134
  }
3135
+ this.openViewModeDropdown(button);
3136
+ }
3137
+ /**
3138
+ * Open the view mode dropdown
3139
+ */
3140
+ openViewModeDropdown(button, focusPlacement = null) {
3141
+ this.closeViewModeDropdown(button);
3001
3142
  button.classList.add("dropdown-active");
3002
3143
  const dropdown = this.createViewModeDropdown(button);
3003
3144
  const rect = button.getBoundingClientRect();
@@ -3005,16 +3146,42 @@ ${blockSuffix}` : suffix;
3005
3146
  dropdown.style.top = `${rect.bottom + 5}px`;
3006
3147
  dropdown.style.left = `${rect.left}px`;
3007
3148
  document.body.appendChild(dropdown);
3149
+ this.activeDropdown = dropdown;
3150
+ this.activeDropdownButton = button;
3151
+ button.setAttribute("aria-controls", dropdown.id);
3152
+ button.setAttribute("aria-expanded", "true");
3008
3153
  this.handleDocumentClick = (e) => {
3009
3154
  if (!dropdown.contains(e.target) && !button.contains(e.target)) {
3010
- dropdown.remove();
3011
- button.classList.remove("dropdown-active");
3012
- document.removeEventListener("click", this.handleDocumentClick);
3155
+ this.closeViewModeDropdown(button);
3013
3156
  }
3014
3157
  };
3015
3158
  setTimeout(() => {
3016
3159
  document.addEventListener("click", this.handleDocumentClick);
3017
3160
  }, 0);
3161
+ if (focusPlacement) {
3162
+ this.focusViewModeMenuItem(dropdown, focusPlacement);
3163
+ }
3164
+ }
3165
+ /**
3166
+ * Close the view mode dropdown
3167
+ */
3168
+ closeViewModeDropdown(button = this.activeDropdownButton, returnFocus = false) {
3169
+ if (this.activeDropdown) {
3170
+ this.activeDropdown.remove();
3171
+ this.activeDropdown = null;
3172
+ }
3173
+ if (button) {
3174
+ button.classList.remove("dropdown-active");
3175
+ button.setAttribute("aria-expanded", "false");
3176
+ }
3177
+ if (this.handleDocumentClick) {
3178
+ document.removeEventListener("click", this.handleDocumentClick);
3179
+ this.handleDocumentClick = null;
3180
+ }
3181
+ this.activeDropdownButton = null;
3182
+ if (returnFocus && button) {
3183
+ button.focus();
3184
+ }
3018
3185
  }
3019
3186
  /**
3020
3187
  * Create view mode dropdown menu (internal implementation)
@@ -3022,6 +3189,12 @@ ${blockSuffix}` : suffix;
3022
3189
  createViewModeDropdown(button) {
3023
3190
  const dropdown = document.createElement("div");
3024
3191
  dropdown.className = "overtype-dropdown-menu";
3192
+ dropdown.id = this.getInstanceElementId("toolbar-view-mode-menu");
3193
+ dropdown.setAttribute("role", "menu");
3194
+ dropdown.setAttribute("aria-label", "View mode");
3195
+ dropdown.addEventListener("keydown", (e) => {
3196
+ this.onViewModeMenuKeydown(e, button);
3197
+ });
3025
3198
  const items = [
3026
3199
  { id: "normal", label: "Normal Edit", icon: "\u2713" },
3027
3200
  { id: "plain", label: "Plain Textarea", icon: "\u2713" },
@@ -3032,12 +3205,15 @@ ${blockSuffix}` : suffix;
3032
3205
  const menuItem = document.createElement("button");
3033
3206
  menuItem.className = "overtype-dropdown-item";
3034
3207
  menuItem.type = "button";
3208
+ menuItem.tabIndex = -1;
3209
+ menuItem.setAttribute("role", "menuitemradio");
3210
+ menuItem.setAttribute("aria-checked", String(item.id === currentMode));
3035
3211
  menuItem.textContent = item.label;
3036
3212
  if (item.id === currentMode) {
3037
3213
  menuItem.classList.add("active");
3038
- menuItem.setAttribute("aria-current", "true");
3039
3214
  const checkmark = document.createElement("span");
3040
3215
  checkmark.className = "overtype-dropdown-icon";
3216
+ checkmark.setAttribute("aria-hidden", "true");
3041
3217
  checkmark.textContent = item.icon;
3042
3218
  menuItem.prepend(checkmark);
3043
3219
  }
@@ -3055,14 +3231,77 @@ ${blockSuffix}` : suffix;
3055
3231
  this.editor.showNormalEditMode();
3056
3232
  break;
3057
3233
  }
3058
- dropdown.remove();
3059
- button.classList.remove("dropdown-active");
3060
- document.removeEventListener("click", this.handleDocumentClick);
3234
+ this.closeViewModeDropdown(button, true);
3061
3235
  });
3062
3236
  dropdown.appendChild(menuItem);
3063
3237
  });
3064
3238
  return dropdown;
3065
3239
  }
3240
+ /**
3241
+ * Handle keyboard navigation inside the view mode menu
3242
+ */
3243
+ onViewModeMenuKeydown(e, button) {
3244
+ const menuItems = this.getViewModeMenuItems();
3245
+ const currentIndex = menuItems.indexOf(e.target);
3246
+ if (currentIndex === -1) {
3247
+ return;
3248
+ }
3249
+ switch (e.key) {
3250
+ case "ArrowDown":
3251
+ e.preventDefault();
3252
+ this.focusViewModeMenuItem(this.activeDropdown, currentIndex + 1);
3253
+ break;
3254
+ case "ArrowUp":
3255
+ e.preventDefault();
3256
+ this.focusViewModeMenuItem(this.activeDropdown, currentIndex - 1);
3257
+ break;
3258
+ case "Home":
3259
+ e.preventDefault();
3260
+ this.focusViewModeMenuItem(this.activeDropdown, "first");
3261
+ break;
3262
+ case "End":
3263
+ e.preventDefault();
3264
+ this.focusViewModeMenuItem(this.activeDropdown, "last");
3265
+ break;
3266
+ case "Escape":
3267
+ e.preventDefault();
3268
+ this.closeViewModeDropdown(button, true);
3269
+ break;
3270
+ }
3271
+ }
3272
+ /**
3273
+ * Focus a view mode menu item by index or placement
3274
+ */
3275
+ focusViewModeMenuItem(dropdown, placement) {
3276
+ const menuItems = this.getViewModeMenuItems(dropdown);
3277
+ if (menuItems.length === 0) {
3278
+ return;
3279
+ }
3280
+ let index = placement;
3281
+ if (placement === "first") {
3282
+ index = 0;
3283
+ } else if (placement === "last") {
3284
+ index = menuItems.length - 1;
3285
+ } else if (placement === "current") {
3286
+ index = menuItems.findIndex((item) => item.getAttribute("aria-checked") === "true");
3287
+ }
3288
+ if (index < 0) {
3289
+ index = menuItems.length - 1;
3290
+ }
3291
+ if (index >= menuItems.length) {
3292
+ index = 0;
3293
+ }
3294
+ menuItems[index].focus();
3295
+ }
3296
+ /**
3297
+ * Get the current view mode menu items
3298
+ */
3299
+ getViewModeMenuItems(dropdown = this.activeDropdown) {
3300
+ if (!dropdown) {
3301
+ return [];
3302
+ }
3303
+ return Array.from(dropdown.querySelectorAll('[role="menuitemradio"]'));
3304
+ }
3066
3305
  /**
3067
3306
  * Update active states of toolbar buttons
3068
3307
  */
@@ -3076,39 +3315,14 @@ ${blockSuffix}` : suffix;
3076
3315
  Object.entries(this.buttons).forEach(([name, button]) => {
3077
3316
  if (name === "viewMode")
3078
3317
  return;
3079
- let isActive = false;
3080
- switch (name) {
3081
- case "bold":
3082
- isActive = activeFormats.includes("bold");
3083
- break;
3084
- case "italic":
3085
- isActive = activeFormats.includes("italic");
3086
- break;
3087
- case "code":
3088
- isActive = false;
3089
- break;
3090
- case "bulletList":
3091
- isActive = activeFormats.includes("bullet-list");
3092
- break;
3093
- case "orderedList":
3094
- isActive = activeFormats.includes("numbered-list");
3095
- break;
3096
- case "taskList":
3097
- isActive = activeFormats.includes("task-list");
3098
- break;
3099
- case "quote":
3100
- isActive = activeFormats.includes("quote");
3101
- break;
3102
- case "h1":
3103
- isActive = activeFormats.includes("header");
3104
- break;
3105
- case "h2":
3106
- isActive = activeFormats.includes("header-2");
3107
- break;
3108
- case "h3":
3109
- isActive = activeFormats.includes("header-3");
3110
- break;
3318
+ const buttonConfig = this.toolbarButtons.find((buttonConfig2) => buttonConfig2.name === name);
3319
+ if (!(buttonConfig == null ? void 0 : buttonConfig.isActive)) {
3320
+ return;
3111
3321
  }
3322
+ const isActive = Boolean(buttonConfig.isActive({
3323
+ editor: this.editor,
3324
+ activeFormats
3325
+ }));
3112
3326
  button.classList.toggle("active", isActive);
3113
3327
  button.setAttribute("aria-pressed", isActive.toString());
3114
3328
  });
@@ -3130,14 +3344,21 @@ ${blockSuffix}` : suffix;
3130
3344
  */
3131
3345
  destroy() {
3132
3346
  if (this.container) {
3133
- if (this.handleDocumentClick) {
3347
+ if (this.activeDropdown) {
3348
+ this.closeViewModeDropdown();
3349
+ } else if (this.handleDocumentClick) {
3134
3350
  document.removeEventListener("click", this.handleDocumentClick);
3351
+ this.handleDocumentClick = null;
3135
3352
  }
3136
3353
  Object.values(this.buttons).forEach((button) => {
3137
3354
  if (button._clickHandler) {
3138
3355
  button.removeEventListener("click", button._clickHandler);
3139
3356
  delete button._clickHandler;
3140
3357
  }
3358
+ if (button._keydownHandler) {
3359
+ button.removeEventListener("keydown", button._keydownHandler);
3360
+ delete button._keydownHandler;
3361
+ }
3141
3362
  });
3142
3363
  this.container.remove();
3143
3364
  this.container = null;
@@ -4404,7 +4625,10 @@ ${blockSuffix}` : suffix;
4404
4625
  e.preventDefault();
4405
4626
  e.stopPropagation();
4406
4627
  if (this.currentLink) {
4407
- window.open(this.currentLink.url, "_blank");
4628
+ const safeUrl = MarkdownParser.sanitizeUrl(this.currentLink.url);
4629
+ if (safeUrl !== "#") {
4630
+ window.open(safeUrl, "_blank");
4631
+ }
4408
4632
  this.hide();
4409
4633
  }
4410
4634
  });
@@ -4432,7 +4656,7 @@ ${blockSuffix}` : suffix;
4432
4656
  if (position >= start && position <= end) {
4433
4657
  return {
4434
4658
  text: match[1],
4435
- url: match[2],
4659
+ url: this.transformUrl(match[2]),
4436
4660
  index: linkIndex,
4437
4661
  start,
4438
4662
  end
@@ -4442,6 +4666,18 @@ ${blockSuffix}` : suffix;
4442
4666
  }
4443
4667
  return null;
4444
4668
  }
4669
+ transformUrl(url) {
4670
+ const transform = this.editor.options.transformLinkUrl;
4671
+ if (typeof transform !== "function")
4672
+ return url;
4673
+ try {
4674
+ const result = transform(url);
4675
+ return typeof result === "string" ? result : url;
4676
+ } catch (e) {
4677
+ console.warn("transformLinkUrl threw:", e);
4678
+ return url;
4679
+ }
4680
+ }
4445
4681
  async show(linkInfo) {
4446
4682
  this.currentLink = linkInfo;
4447
4683
  this.cancelHide();
@@ -4593,6 +4829,7 @@ ${blockSuffix}` : suffix;
4593
4829
  actionId: "toggleBold",
4594
4830
  icon: boldIcon,
4595
4831
  title: "Bold (Ctrl+B)",
4832
+ isActive: ({ activeFormats }) => activeFormats.includes("bold"),
4596
4833
  action: ({ editor }) => {
4597
4834
  toggleBold(editor.textarea);
4598
4835
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4603,6 +4840,7 @@ ${blockSuffix}` : suffix;
4603
4840
  actionId: "toggleItalic",
4604
4841
  icon: italicIcon,
4605
4842
  title: "Italic (Ctrl+I)",
4843
+ isActive: ({ activeFormats }) => activeFormats.includes("italic"),
4606
4844
  action: ({ editor }) => {
4607
4845
  toggleItalic(editor.textarea);
4608
4846
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4613,6 +4851,7 @@ ${blockSuffix}` : suffix;
4613
4851
  actionId: "toggleCode",
4614
4852
  icon: codeIcon,
4615
4853
  title: "Inline Code",
4854
+ isActive: () => false,
4616
4855
  action: ({ editor }) => {
4617
4856
  toggleCode(editor.textarea);
4618
4857
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4637,6 +4876,7 @@ ${blockSuffix}` : suffix;
4637
4876
  actionId: "toggleH1",
4638
4877
  icon: h1Icon,
4639
4878
  title: "Heading 1",
4879
+ isActive: ({ activeFormats }) => activeFormats.includes("header"),
4640
4880
  action: ({ editor }) => {
4641
4881
  toggleH1(editor.textarea);
4642
4882
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4647,6 +4887,7 @@ ${blockSuffix}` : suffix;
4647
4887
  actionId: "toggleH2",
4648
4888
  icon: h2Icon,
4649
4889
  title: "Heading 2",
4890
+ isActive: ({ activeFormats }) => activeFormats.includes("header-2"),
4650
4891
  action: ({ editor }) => {
4651
4892
  toggleH2(editor.textarea);
4652
4893
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4657,6 +4898,7 @@ ${blockSuffix}` : suffix;
4657
4898
  actionId: "toggleH3",
4658
4899
  icon: h3Icon,
4659
4900
  title: "Heading 3",
4901
+ isActive: ({ activeFormats }) => activeFormats.includes("header-3"),
4660
4902
  action: ({ editor }) => {
4661
4903
  toggleH3(editor.textarea);
4662
4904
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4667,6 +4909,7 @@ ${blockSuffix}` : suffix;
4667
4909
  actionId: "toggleBulletList",
4668
4910
  icon: bulletListIcon,
4669
4911
  title: "Bullet List",
4912
+ isActive: ({ activeFormats }) => activeFormats.includes("bullet-list"),
4670
4913
  action: ({ editor }) => {
4671
4914
  toggleBulletList(editor.textarea);
4672
4915
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4677,6 +4920,7 @@ ${blockSuffix}` : suffix;
4677
4920
  actionId: "toggleNumberedList",
4678
4921
  icon: orderedListIcon,
4679
4922
  title: "Numbered List",
4923
+ isActive: ({ activeFormats }) => activeFormats.includes("numbered-list"),
4680
4924
  action: ({ editor }) => {
4681
4925
  toggleNumberedList(editor.textarea);
4682
4926
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4687,6 +4931,7 @@ ${blockSuffix}` : suffix;
4687
4931
  actionId: "toggleTaskList",
4688
4932
  icon: taskListIcon,
4689
4933
  title: "Task List",
4934
+ isActive: ({ activeFormats }) => activeFormats.includes("task-list"),
4690
4935
  action: ({ editor }) => {
4691
4936
  if (toggleTaskList) {
4692
4937
  toggleTaskList(editor.textarea);
@@ -4699,6 +4944,7 @@ ${blockSuffix}` : suffix;
4699
4944
  actionId: "toggleQuote",
4700
4945
  icon: quoteIcon,
4701
4946
  title: "Quote",
4947
+ isActive: ({ activeFormats }) => activeFormats.includes("quote"),
4702
4948
  action: ({ editor }) => {
4703
4949
  toggleQuote(editor.textarea);
4704
4950
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4760,6 +5006,17 @@ ${blockSuffix}` : suffix;
4760
5006
  ];
4761
5007
 
4762
5008
  // src/overtype.js
5009
+ var _isSafariCache;
5010
+ function isSafariBrowser() {
5011
+ if (_isSafariCache !== void 0)
5012
+ return _isSafariCache;
5013
+ _isSafariCache = false;
5014
+ if (typeof navigator !== "undefined") {
5015
+ const ua = navigator.userAgent || "";
5016
+ _isSafariCache = /^((?!chrome|android|crios|fxios|edg|opr).)*safari/i.test(ua) || /iPad|iPhone|iPod/.test(ua) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1 && /Safari/.test(ua);
5017
+ }
5018
+ return _isSafariCache;
5019
+ }
4763
5020
  function buildActionsMap(buttons) {
4764
5021
  const map = {};
4765
5022
  (buttons || []).forEach((btn) => {
@@ -4834,6 +5091,8 @@ ${blockSuffix}` : suffix;
4834
5091
  this.options = this._mergeOptions(options);
4835
5092
  this.instanceId = ++_OverType.instanceCount;
4836
5093
  this.initialized = false;
5094
+ this._isSafari = isSafariBrowser();
5095
+ this._safariReflowRaf = null;
4837
5096
  _OverType.injectStyles();
4838
5097
  _OverType.initGlobalListeners();
4839
5098
  const container = element.querySelector(".overtype-container");
@@ -4908,8 +5167,10 @@ ${blockSuffix}` : suffix;
4908
5167
  // Enable smart list continuation
4909
5168
  codeHighlighter: null,
4910
5169
  // Per-instance code highlighter
4911
- spellcheck: false
5170
+ spellcheck: false,
4912
5171
  // Browser spellcheck (disabled by default)
5172
+ transformLinkUrl: null
5173
+ // Transform URLs shown/opened in the link tooltip
4913
5174
  };
4914
5175
  const { theme, colors, ...cleanOptions } = options;
4915
5176
  return {
@@ -4962,6 +5223,8 @@ ${blockSuffix}` : suffix;
4962
5223
  this.wrapper._instance = this;
4963
5224
  this._applyInstanceCSSVars();
4964
5225
  this._configureTextarea();
5226
+ this._ensureTextareaId();
5227
+ this._syncPreviewInteractivity();
4965
5228
  this._applyOptions();
4966
5229
  }
4967
5230
  /**
@@ -5025,6 +5288,7 @@ ${blockSuffix}` : suffix;
5025
5288
  }
5026
5289
  });
5027
5290
  }
5291
+ this._ensureTextareaId();
5028
5292
  this.preview = document.createElement("div");
5029
5293
  this.preview.className = "overtype-preview";
5030
5294
  this.preview.setAttribute("aria-hidden", "true");
@@ -5048,6 +5312,7 @@ ${blockSuffix}` : suffix;
5048
5312
  } else {
5049
5313
  this.container.classList.remove("overtype-auto-resize");
5050
5314
  }
5315
+ this._syncPreviewInteractivity();
5051
5316
  }
5052
5317
  /**
5053
5318
  * Configure textarea attributes
@@ -5062,6 +5327,31 @@ ${blockSuffix}` : suffix;
5062
5327
  this.textarea.setAttribute("data-gramm_editor", "false");
5063
5328
  this.textarea.setAttribute("data-enable-grammarly", "false");
5064
5329
  }
5330
+ /**
5331
+ * Ensure the textarea can be referenced by aria-controls
5332
+ * @private
5333
+ */
5334
+ _ensureTextareaId() {
5335
+ if (!this.textarea.id) {
5336
+ this.textarea.id = `overtype-${this.instanceId}-input`;
5337
+ }
5338
+ }
5339
+ /**
5340
+ * Keep rendered preview content out of keyboard navigation until Preview mode.
5341
+ * @private
5342
+ */
5343
+ _syncPreviewInteractivity() {
5344
+ if (!this.preview || !this.container)
5345
+ return;
5346
+ const isPreviewMode = this.container.dataset.mode === "preview";
5347
+ this.preview.inert = !isPreviewMode;
5348
+ this.preview.toggleAttribute("inert", !isPreviewMode);
5349
+ if (isPreviewMode) {
5350
+ this.preview.removeAttribute("aria-hidden");
5351
+ return;
5352
+ }
5353
+ this.preview.setAttribute("aria-hidden", "true");
5354
+ }
5065
5355
  /**
5066
5356
  * Create and setup toolbar
5067
5357
  * @private
@@ -5400,6 +5690,29 @@ ${blockSuffix}` : suffix;
5400
5690
  handleInput(event) {
5401
5691
  this.updatePreview();
5402
5692
  this._notifyChange();
5693
+ this._scheduleSafariReflow();
5694
+ }
5695
+ /**
5696
+ * Force Safari to re-shape stale textarea text after an edit.
5697
+ * Safari can leave a textarea's glyph layout cached after incremental edits,
5698
+ * desyncing the caret/wrap from the styled preview overlay. Toggling
5699
+ * letter-spacing (with !important to beat the stylesheet rule) and reading
5700
+ * offsetHeight forces a synchronous re-shape. Safari-only, coalesced to one
5701
+ * run per animation frame.
5702
+ * @private
5703
+ */
5704
+ _scheduleSafariReflow() {
5705
+ if (!this._isSafari || this._safariReflowRaf)
5706
+ return;
5707
+ this._safariReflowRaf = requestAnimationFrame(() => {
5708
+ this._safariReflowRaf = null;
5709
+ const ta = this.textarea;
5710
+ if (!ta)
5711
+ return;
5712
+ ta.style.setProperty("letter-spacing", "-0.001px", "important");
5713
+ void ta.offsetHeight;
5714
+ ta.style.removeProperty("letter-spacing");
5715
+ });
5403
5716
  }
5404
5717
  /**
5405
5718
  * Handle focus events
@@ -5427,49 +5740,11 @@ ${blockSuffix}` : suffix;
5427
5740
  if (event.key === "Tab") {
5428
5741
  const start = this.textarea.selectionStart;
5429
5742
  const end = this.textarea.selectionEnd;
5430
- const value = this.textarea.value;
5431
- if (event.shiftKey && start === end) {
5743
+ if (start !== end && this._canEditTextarea()) {
5744
+ event.preventDefault();
5745
+ event.shiftKey ? this.outdentSelection() : this.indentSelection();
5432
5746
  return;
5433
5747
  }
5434
- event.preventDefault();
5435
- if (start !== end && event.shiftKey) {
5436
- const before = value.substring(0, start);
5437
- const selection = value.substring(start, end);
5438
- const after = value.substring(end);
5439
- const lines = selection.split("\n");
5440
- const outdented = lines.map((line) => line.replace(/^ /, "")).join("\n");
5441
- if (document.execCommand) {
5442
- this.textarea.setSelectionRange(start, end);
5443
- document.execCommand("insertText", false, outdented);
5444
- } else {
5445
- this.textarea.value = before + outdented + after;
5446
- this.textarea.selectionStart = start;
5447
- this.textarea.selectionEnd = start + outdented.length;
5448
- }
5449
- } else if (start !== end) {
5450
- const before = value.substring(0, start);
5451
- const selection = value.substring(start, end);
5452
- const after = value.substring(end);
5453
- const lines = selection.split("\n");
5454
- const indented = lines.map((line) => " " + line).join("\n");
5455
- if (document.execCommand) {
5456
- this.textarea.setSelectionRange(start, end);
5457
- document.execCommand("insertText", false, indented);
5458
- } else {
5459
- this.textarea.value = before + indented + after;
5460
- this.textarea.selectionStart = start;
5461
- this.textarea.selectionEnd = start + indented.length;
5462
- }
5463
- } else {
5464
- if (document.execCommand) {
5465
- document.execCommand("insertText", false, " ");
5466
- } else {
5467
- this.textarea.value = value.substring(0, start) + " " + value.substring(end);
5468
- this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
5469
- }
5470
- }
5471
- this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5472
- return;
5473
5748
  }
5474
5749
  if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
5475
5750
  if (this.handleSmartListContinuation()) {
@@ -5606,6 +5881,7 @@ ${blockSuffix}` : suffix;
5606
5881
  }
5607
5882
  if (didChange) {
5608
5883
  this._notifyChange();
5884
+ this._scheduleSafariReflow();
5609
5885
  }
5610
5886
  }
5611
5887
  /**
@@ -5666,6 +5942,63 @@ ${blockSuffix}` : suffix;
5666
5942
  getPreviewHTML() {
5667
5943
  return this.preview.innerHTML;
5668
5944
  }
5945
+ /**
5946
+ * Indent the current line or selected lines by two spaces.
5947
+ */
5948
+ indentSelection() {
5949
+ this._replaceSelectedLines((line) => ` ${line}`);
5950
+ }
5951
+ /**
5952
+ * Outdent the current line or selected lines by up to two spaces or one tab.
5953
+ */
5954
+ outdentSelection() {
5955
+ this._replaceSelectedLines((line) => line.replace(/^( {1,2}|\t)/, ""));
5956
+ }
5957
+ /**
5958
+ * Replace full lines touched by the current selection.
5959
+ * @private
5960
+ */
5961
+ _replaceSelectedLines(transformLine) {
5962
+ if (!this._canEditTextarea())
5963
+ return false;
5964
+ const textarea = this.textarea;
5965
+ const { selectionStart, selectionEnd, value } = textarea;
5966
+ const lineStart = value.lastIndexOf("\n", selectionStart - 1) + 1;
5967
+ const effectiveEnd = this._effectiveSelectionEnd(value, selectionStart, selectionEnd);
5968
+ const lineEndOffset = value.indexOf("\n", effectiveEnd);
5969
+ const lineEnd = lineEndOffset === -1 ? value.length : lineEndOffset;
5970
+ const selectedLines = value.slice(lineStart, lineEnd);
5971
+ const replacement = selectedLines.split("\n").map(transformLine).join("\n");
5972
+ if (replacement === selectedLines)
5973
+ return false;
5974
+ textarea.setSelectionRange(lineStart, lineEnd);
5975
+ let inserted = false;
5976
+ try {
5977
+ inserted = document.execCommand("insertText", false, replacement);
5978
+ } catch (_) {
5979
+ }
5980
+ if (!inserted) {
5981
+ textarea.setRangeText(replacement, lineStart, lineEnd, "preserve");
5982
+ }
5983
+ textarea.setSelectionRange(lineStart, lineStart + replacement.length);
5984
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
5985
+ return true;
5986
+ }
5987
+ /**
5988
+ * @private
5989
+ */
5990
+ _effectiveSelectionEnd(value, selectionStart, selectionEnd) {
5991
+ if (selectionEnd > selectionStart && value[selectionEnd - 1] === "\n") {
5992
+ return selectionEnd - 1;
5993
+ }
5994
+ return selectionEnd;
5995
+ }
5996
+ /**
5997
+ * @private
5998
+ */
5999
+ _canEditTextarea() {
6000
+ return this.textarea && !this.textarea.disabled && !this.textarea.readOnly;
6001
+ }
5669
6002
  /**
5670
6003
  * Get clean HTML without any OverType-specific markup
5671
6004
  * Useful for exporting to other formats or storage
@@ -5889,6 +6222,7 @@ ${blockSuffix}` : suffix;
5889
6222
  */
5890
6223
  showNormalEditMode() {
5891
6224
  this.container.dataset.mode = "normal";
6225
+ this._syncPreviewInteractivity();
5892
6226
  this.updatePreview();
5893
6227
  this._updateAutoHeight();
5894
6228
  requestAnimationFrame(() => {
@@ -5903,6 +6237,7 @@ ${blockSuffix}` : suffix;
5903
6237
  */
5904
6238
  showPlainTextarea() {
5905
6239
  this.container.dataset.mode = "plain";
6240
+ this._syncPreviewInteractivity();
5906
6241
  this._updateAutoHeight();
5907
6242
  if (this.toolbar) {
5908
6243
  const toggleBtn = this.container.querySelector('[data-action="toggle-plain"]');
@@ -5919,6 +6254,7 @@ ${blockSuffix}` : suffix;
5919
6254
  */
5920
6255
  showPreviewMode() {
5921
6256
  this.container.dataset.mode = "preview";
6257
+ this._syncPreviewInteractivity();
5922
6258
  this.updatePreview();
5923
6259
  this._updateAutoHeight();
5924
6260
  return this;
@@ -5937,6 +6273,10 @@ ${blockSuffix}` : suffix;
5937
6273
  if (this.shortcuts) {
5938
6274
  this.shortcuts.destroy();
5939
6275
  }
6276
+ if (this._safariReflowRaf) {
6277
+ cancelAnimationFrame(this._safariReflowRaf);
6278
+ this._safariReflowRaf = null;
6279
+ }
5940
6280
  if (this.wrapper) {
5941
6281
  const content = this.getValue();
5942
6282
  this.wrapper.remove();
@@ -5968,11 +6308,16 @@ ${blockSuffix}` : suffix;
5968
6308
  return elements.map((el) => {
5969
6309
  const options = { ...defaults };
5970
6310
  for (const attr of el.attributes) {
5971
- if (attr.name.startsWith("data-ot-")) {
5972
- const kebab = attr.name.slice(8);
5973
- const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
5974
- options[key] = _OverType._parseDataValue(attr.value);
6311
+ if (!attr.name.startsWith("data-ot-"))
6312
+ continue;
6313
+ const kebab = attr.name.slice(8);
6314
+ if (kebab.startsWith("textarea-") && kebab !== "textarea-props") {
6315
+ const propKey = kebab.slice(9).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6316
+ options.textareaProps = { ...options.textareaProps || {}, [propKey]: _OverType._parseDataValue(attr.value) };
6317
+ continue;
5975
6318
  }
6319
+ const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6320
+ options[key] = _OverType._parseDataValue(attr.value);
5976
6321
  }
5977
6322
  return new _OverType(el, options)[0];
5978
6323
  });
@@ -6017,6 +6362,14 @@ ${blockSuffix}` : suffix;
6017
6362
  return null;
6018
6363
  if (value !== "" && !isNaN(Number(value)))
6019
6364
  return Number(value);
6365
+ const trimmed = value.trim();
6366
+ if (trimmed[0] === "{" || trimmed[0] === "[") {
6367
+ try {
6368
+ return JSON.parse(trimmed);
6369
+ } catch (e) {
6370
+ return value;
6371
+ }
6372
+ }
6020
6373
  return value;
6021
6374
  }
6022
6375
  /**
@@ -6195,8 +6548,13 @@ ${blockSuffix}` : suffix;
6195
6548
  * Initialize global event listeners
6196
6549
  */
6197
6550
  static initGlobalListeners() {
6198
- if (_OverType.globalListenersInitialized)
6551
+ const globalScope = typeof window !== "undefined" ? window : globalThis;
6552
+ const globalListenersKey = "__overtypeGlobalListenersInitialized";
6553
+ if (_OverType.globalListenersInitialized || globalScope[globalListenersKey]) {
6554
+ _OverType.globalListenersInitialized = true;
6199
6555
  return;
6556
+ }
6557
+ globalScope[globalListenersKey] = true;
6200
6558
  document.addEventListener("input", (e) => {
6201
6559
  if (e.target && e.target.classList && e.target.classList.contains("overtype-input")) {
6202
6560
  const wrapper = e.target.closest(".overtype-wrapper");