overtype 2.3.9 → 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.9
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
@@ -5191,6 +5481,7 @@ ${blockSuffix}` : suffix;
5191
5481
  return;
5192
5482
  }
5193
5483
  this._fileUploadCounter = 0;
5484
+ this._uploadedFiles = /* @__PURE__ */ new Map();
5194
5485
  this._boundHandleFilePaste = this._handleFilePaste.bind(this);
5195
5486
  this._boundHandleFileDrop = this._handleFileDrop.bind(this);
5196
5487
  this._boundHandleDragOver = this._handleDragOver.bind(this);
@@ -5199,6 +5490,53 @@ ${blockSuffix}` : suffix;
5199
5490
  this.textarea.addEventListener("dragover", this._boundHandleDragOver);
5200
5491
  this.fileUploadInitialized = true;
5201
5492
  }
5493
+ /**
5494
+ * Extract URLs from markdown link syntax: [text](url) or ![text](url).
5495
+ * @private
5496
+ */
5497
+ _extractMarkdownUrls(text) {
5498
+ const urls = [];
5499
+ const re = /!?\[[^\]]*\]\(([^)\s]+)/g;
5500
+ let m;
5501
+ while ((m = re.exec(text)) !== null)
5502
+ urls.push(m[1]);
5503
+ return urls;
5504
+ }
5505
+ /**
5506
+ * Track URLs that were just inserted, pairing each with the source File.
5507
+ * If multiple URLs appear in one inserted block, all get associated with
5508
+ * the same file (rare; happens if onInsertFile returns several links).
5509
+ * @private
5510
+ */
5511
+ _trackInsertedUrls(insertedText, file) {
5512
+ if (!this._uploadedFiles || !file || !insertedText)
5513
+ return;
5514
+ for (const url of this._extractMarkdownUrls(insertedText)) {
5515
+ this._uploadedFiles.set(url, { filename: file.name, file });
5516
+ }
5517
+ }
5518
+ /**
5519
+ * Diff the tracked-URL set against the current value and fire
5520
+ * fileUpload.onRemoveFile for any URL no longer present.
5521
+ * @private
5522
+ */
5523
+ _checkForRemovedUploads() {
5524
+ var _a;
5525
+ if (!this._uploadedFiles || this._uploadedFiles.size === 0)
5526
+ return;
5527
+ const cb = (_a = this.options.fileUpload) == null ? void 0 : _a.onRemoveFile;
5528
+ const value = this.textarea.value;
5529
+ const removed = [];
5530
+ for (const [url, info] of this._uploadedFiles) {
5531
+ if (!value.includes(url))
5532
+ removed.push({ url, info });
5533
+ }
5534
+ for (const { url, info } of removed) {
5535
+ this._uploadedFiles.delete(url);
5536
+ if (cb)
5537
+ cb({ url, filename: info.filename, file: info.file });
5538
+ }
5539
+ }
5202
5540
  _handleFilePaste(e) {
5203
5541
  var _a, _b;
5204
5542
  if (!((_b = (_a = e == null ? void 0 : e.clipboardData) == null ? void 0 : _a.files) == null ? void 0 : _b.length))
@@ -5231,6 +5569,7 @@ ${blockSuffix}` : suffix;
5231
5569
  }
5232
5570
  this.options.fileUpload.onInsertFile(file).then((text) => {
5233
5571
  this.textarea.value = this.textarea.value.replace(placeholder, text);
5572
+ this._trackInsertedUrls(text, file);
5234
5573
  this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5235
5574
  }, (error) => {
5236
5575
  console.error("OverType: File upload failed", error);
@@ -5243,6 +5582,7 @@ ${blockSuffix}` : suffix;
5243
5582
  const texts = Array.isArray(result) ? result : [result];
5244
5583
  texts.forEach((text, index) => {
5245
5584
  this.textarea.value = this.textarea.value.replace(files[index].placeholder, text);
5585
+ this._trackInsertedUrls(text, files[index].file);
5246
5586
  });
5247
5587
  this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5248
5588
  }, (error) => {
@@ -5264,6 +5604,7 @@ ${blockSuffix}` : suffix;
5264
5604
  this._boundHandleFilePaste = null;
5265
5605
  this._boundHandleFileDrop = null;
5266
5606
  this._boundHandleDragOver = null;
5607
+ this._uploadedFiles = null;
5267
5608
  this.fileUploadInitialized = false;
5268
5609
  }
5269
5610
  insertAtCursor(text) {
@@ -5308,9 +5649,12 @@ ${blockSuffix}` : suffix;
5308
5649
  * @private
5309
5650
  */
5310
5651
  _notifyChange() {
5311
- if (!this.options.onChange || !this.initialized)
5652
+ if (!this.initialized)
5312
5653
  return;
5313
- this.options.onChange(this.textarea.value, this);
5654
+ this._checkForRemovedUploads();
5655
+ if (this.options.onChange) {
5656
+ this.options.onChange(this.textarea.value, this);
5657
+ }
5314
5658
  }
5315
5659
  /**
5316
5660
  * Apply background styling to code blocks
@@ -5346,6 +5690,29 @@ ${blockSuffix}` : suffix;
5346
5690
  handleInput(event) {
5347
5691
  this.updatePreview();
5348
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
+ });
5349
5716
  }
5350
5717
  /**
5351
5718
  * Handle focus events
@@ -5373,49 +5740,11 @@ ${blockSuffix}` : suffix;
5373
5740
  if (event.key === "Tab") {
5374
5741
  const start = this.textarea.selectionStart;
5375
5742
  const end = this.textarea.selectionEnd;
5376
- const value = this.textarea.value;
5377
- if (event.shiftKey && start === end) {
5743
+ if (start !== end && this._canEditTextarea()) {
5744
+ event.preventDefault();
5745
+ event.shiftKey ? this.outdentSelection() : this.indentSelection();
5378
5746
  return;
5379
5747
  }
5380
- event.preventDefault();
5381
- if (start !== end && event.shiftKey) {
5382
- const before = value.substring(0, start);
5383
- const selection = value.substring(start, end);
5384
- const after = value.substring(end);
5385
- const lines = selection.split("\n");
5386
- const outdented = lines.map((line) => line.replace(/^ /, "")).join("\n");
5387
- if (document.execCommand) {
5388
- this.textarea.setSelectionRange(start, end);
5389
- document.execCommand("insertText", false, outdented);
5390
- } else {
5391
- this.textarea.value = before + outdented + after;
5392
- this.textarea.selectionStart = start;
5393
- this.textarea.selectionEnd = start + outdented.length;
5394
- }
5395
- } else if (start !== end) {
5396
- const before = value.substring(0, start);
5397
- const selection = value.substring(start, end);
5398
- const after = value.substring(end);
5399
- const lines = selection.split("\n");
5400
- const indented = lines.map((line) => " " + line).join("\n");
5401
- if (document.execCommand) {
5402
- this.textarea.setSelectionRange(start, end);
5403
- document.execCommand("insertText", false, indented);
5404
- } else {
5405
- this.textarea.value = before + indented + after;
5406
- this.textarea.selectionStart = start;
5407
- this.textarea.selectionEnd = start + indented.length;
5408
- }
5409
- } else {
5410
- if (document.execCommand) {
5411
- document.execCommand("insertText", false, " ");
5412
- } else {
5413
- this.textarea.value = value.substring(0, start) + " " + value.substring(end);
5414
- this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
5415
- }
5416
- }
5417
- this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5418
- return;
5419
5748
  }
5420
5749
  if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
5421
5750
  if (this.handleSmartListContinuation()) {
@@ -5552,6 +5881,7 @@ ${blockSuffix}` : suffix;
5552
5881
  }
5553
5882
  if (didChange) {
5554
5883
  this._notifyChange();
5884
+ this._scheduleSafariReflow();
5555
5885
  }
5556
5886
  }
5557
5887
  /**
@@ -5612,6 +5942,63 @@ ${blockSuffix}` : suffix;
5612
5942
  getPreviewHTML() {
5613
5943
  return this.preview.innerHTML;
5614
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
+ }
5615
6002
  /**
5616
6003
  * Get clean HTML without any OverType-specific markup
5617
6004
  * Useful for exporting to other formats or storage
@@ -5835,6 +6222,7 @@ ${blockSuffix}` : suffix;
5835
6222
  */
5836
6223
  showNormalEditMode() {
5837
6224
  this.container.dataset.mode = "normal";
6225
+ this._syncPreviewInteractivity();
5838
6226
  this.updatePreview();
5839
6227
  this._updateAutoHeight();
5840
6228
  requestAnimationFrame(() => {
@@ -5849,6 +6237,7 @@ ${blockSuffix}` : suffix;
5849
6237
  */
5850
6238
  showPlainTextarea() {
5851
6239
  this.container.dataset.mode = "plain";
6240
+ this._syncPreviewInteractivity();
5852
6241
  this._updateAutoHeight();
5853
6242
  if (this.toolbar) {
5854
6243
  const toggleBtn = this.container.querySelector('[data-action="toggle-plain"]');
@@ -5865,6 +6254,7 @@ ${blockSuffix}` : suffix;
5865
6254
  */
5866
6255
  showPreviewMode() {
5867
6256
  this.container.dataset.mode = "preview";
6257
+ this._syncPreviewInteractivity();
5868
6258
  this.updatePreview();
5869
6259
  this._updateAutoHeight();
5870
6260
  return this;
@@ -5883,6 +6273,10 @@ ${blockSuffix}` : suffix;
5883
6273
  if (this.shortcuts) {
5884
6274
  this.shortcuts.destroy();
5885
6275
  }
6276
+ if (this._safariReflowRaf) {
6277
+ cancelAnimationFrame(this._safariReflowRaf);
6278
+ this._safariReflowRaf = null;
6279
+ }
5886
6280
  if (this.wrapper) {
5887
6281
  const content = this.getValue();
5888
6282
  this.wrapper.remove();
@@ -5914,11 +6308,16 @@ ${blockSuffix}` : suffix;
5914
6308
  return elements.map((el) => {
5915
6309
  const options = { ...defaults };
5916
6310
  for (const attr of el.attributes) {
5917
- if (attr.name.startsWith("data-ot-")) {
5918
- const kebab = attr.name.slice(8);
5919
- const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
5920
- 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;
5921
6318
  }
6319
+ const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6320
+ options[key] = _OverType._parseDataValue(attr.value);
5922
6321
  }
5923
6322
  return new _OverType(el, options)[0];
5924
6323
  });
@@ -5963,6 +6362,14 @@ ${blockSuffix}` : suffix;
5963
6362
  return null;
5964
6363
  if (value !== "" && !isNaN(Number(value)))
5965
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
+ }
5966
6373
  return value;
5967
6374
  }
5968
6375
  /**
@@ -6141,8 +6548,13 @@ ${blockSuffix}` : suffix;
6141
6548
  * Initialize global event listeners
6142
6549
  */
6143
6550
  static initGlobalListeners() {
6144
- 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;
6145
6555
  return;
6556
+ }
6557
+ globalScope[globalListenersKey] = true;
6146
6558
  document.addEventListener("input", (e) => {
6147
6559
  if (e.target && e.target.classList && e.target.classList.contains("overtype-input")) {
6148
6560
  const wrapper = e.target.closest(".overtype-wrapper");