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.
package/dist/overtype.cjs CHANGED
@@ -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
@@ -781,6 +781,16 @@ var ShortcutsManager = class {
781
781
  const modKey = isMac ? event.metaKey : event.ctrlKey;
782
782
  if (!modKey)
783
783
  return false;
784
+ if (event.key === "]") {
785
+ event.preventDefault();
786
+ this.editor.indentSelection();
787
+ return true;
788
+ }
789
+ if (event.key === "[") {
790
+ event.preventDefault();
791
+ this.editor.outdentSelection();
792
+ return true;
793
+ }
784
794
  let actionId = null;
785
795
  switch (event.key.toLowerCase()) {
786
796
  case "b":
@@ -2893,6 +2903,10 @@ var Toolbar = class {
2893
2903
  this.editor = editor;
2894
2904
  this.container = null;
2895
2905
  this.buttons = {};
2906
+ this.currentItemIndex = 0;
2907
+ this.handleDocumentClick = null;
2908
+ this.activeDropdown = null;
2909
+ this.activeDropdownButton = null;
2896
2910
  this.toolbarButtons = options.toolbarButtons || [];
2897
2911
  }
2898
2912
  /**
@@ -2901,8 +2915,10 @@ var Toolbar = class {
2901
2915
  create() {
2902
2916
  this.container = document.createElement("div");
2903
2917
  this.container.className = "overtype-toolbar";
2918
+ this.container.id = this.getInstanceElementId("toolbar");
2904
2919
  this.container.setAttribute("role", "toolbar");
2905
2920
  this.container.setAttribute("aria-label", "Formatting toolbar");
2921
+ this.container.setAttribute("aria-controls", this.editor.textarea.id);
2906
2922
  this.toolbarButtons.forEach((buttonConfig) => {
2907
2923
  if (buttonConfig.name === "separator") {
2908
2924
  const separator = this.createSeparator();
@@ -2913,8 +2929,116 @@ var Toolbar = class {
2913
2929
  this.container.appendChild(button);
2914
2930
  }
2915
2931
  });
2932
+ this.setupRovingTabIndex();
2933
+ this.updateButtonStates();
2916
2934
  this.editor.container.insertBefore(this.container, this.editor.wrapper);
2917
2935
  }
2936
+ /**
2937
+ * Build a stable id from the owning OverType instance
2938
+ */
2939
+ getInstanceElementId(name) {
2940
+ return `overtype-${this.editor.instanceId}-${name}`;
2941
+ }
2942
+ /**
2943
+ * Configure toolbar focus management per the ARIA toolbar pattern
2944
+ */
2945
+ setupRovingTabIndex() {
2946
+ const toolbarItems = this.getToolbarItems();
2947
+ if (toolbarItems.length === 0) {
2948
+ return;
2949
+ }
2950
+ this.currentItemIndex = this.getValidItemIndex(this.currentItemIndex);
2951
+ this.updateTabIndexes();
2952
+ this.container.addEventListener("keydown", (e) => {
2953
+ this.onToolbarKeydown(e);
2954
+ });
2955
+ this.container.addEventListener("focusin", (e) => {
2956
+ this.onToolbarFocusin(e);
2957
+ });
2958
+ }
2959
+ /**
2960
+ * Get toolbar buttons in DOM order for keyboard navigation
2961
+ */
2962
+ getToolbarItems() {
2963
+ if (!this.container) {
2964
+ return [];
2965
+ }
2966
+ return Array.from(this.container.querySelectorAll(".overtype-toolbar-button"));
2967
+ }
2968
+ /**
2969
+ * Handle keyboard navigation within the toolbar
2970
+ */
2971
+ onToolbarKeydown(e) {
2972
+ const toolbarItems = this.getToolbarItems();
2973
+ if (!toolbarItems.includes(e.target)) {
2974
+ return;
2975
+ }
2976
+ switch (e.key) {
2977
+ case "ArrowRight":
2978
+ e.preventDefault();
2979
+ this.focusItem(this.currentItemIndex + 1);
2980
+ break;
2981
+ case "ArrowLeft":
2982
+ e.preventDefault();
2983
+ this.focusItem(this.currentItemIndex - 1);
2984
+ break;
2985
+ case "Home":
2986
+ e.preventDefault();
2987
+ this.focusItem(0);
2988
+ break;
2989
+ case "End":
2990
+ e.preventDefault();
2991
+ this.focusItem(toolbarItems.length - 1);
2992
+ break;
2993
+ }
2994
+ }
2995
+ /**
2996
+ * Remember the focused toolbar item as the toolbar tab stop
2997
+ */
2998
+ onToolbarFocusin(e) {
2999
+ const focusedItemIndex = this.getToolbarItems().indexOf(e.target);
3000
+ if (focusedItemIndex === -1) {
3001
+ return;
3002
+ }
3003
+ this.currentItemIndex = focusedItemIndex;
3004
+ this.updateTabIndexes();
3005
+ }
3006
+ /**
3007
+ * Move focus to a toolbar item and make it the only tab stop
3008
+ */
3009
+ focusItem(index) {
3010
+ const toolbarItems = this.getToolbarItems();
3011
+ if (toolbarItems.length === 0) {
3012
+ return;
3013
+ }
3014
+ this.currentItemIndex = this.getValidItemIndex(index, toolbarItems);
3015
+ this.updateTabIndexes();
3016
+ toolbarItems[this.currentItemIndex].focus();
3017
+ }
3018
+ /**
3019
+ * Normalize toolbar item indexes with wrapping
3020
+ */
3021
+ getValidItemIndex(index, toolbarItems = this.getToolbarItems()) {
3022
+ const itemCount = toolbarItems.length;
3023
+ if (itemCount === 0) {
3024
+ return 0;
3025
+ }
3026
+ if (index < 0) {
3027
+ return itemCount - 1;
3028
+ }
3029
+ if (index >= itemCount) {
3030
+ return 0;
3031
+ }
3032
+ return index;
3033
+ }
3034
+ /**
3035
+ * Keep exactly one toolbar item in the page tab sequence
3036
+ */
3037
+ updateTabIndexes() {
3038
+ this.getToolbarItems().forEach((item, index) => {
3039
+ item.tabIndex = index === this.currentItemIndex ? 0 : -1;
3040
+ });
3041
+ }
2918
3042
  /**
2919
3043
  * Create a toolbar separator
2920
3044
  */
@@ -2938,10 +3062,22 @@ var Toolbar = class {
2938
3062
  if (buttonConfig.name === "viewMode") {
2939
3063
  button.classList.add("has-dropdown");
2940
3064
  button.dataset.dropdown = "true";
2941
- button.addEventListener("click", (e) => {
3065
+ button.setAttribute("aria-haspopup", "menu");
3066
+ button.setAttribute("aria-expanded", "false");
3067
+ button._clickHandler = (e) => {
2942
3068
  e.preventDefault();
2943
3069
  this.toggleViewModeDropdown(button);
2944
- });
3070
+ };
3071
+ button._keydownHandler = (e) => {
3072
+ if (!["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
3073
+ return;
3074
+ }
3075
+ e.preventDefault();
3076
+ const placement = e.key === "ArrowUp" ? "last" : "current";
3077
+ this.openViewModeDropdown(button, placement);
3078
+ };
3079
+ button.addEventListener("click", button._clickHandler);
3080
+ button.addEventListener("keydown", button._keydownHandler);
2945
3081
  return button;
2946
3082
  }
2947
3083
  button._clickHandler = (e) => {
@@ -2996,12 +3132,17 @@ var Toolbar = class {
2996
3132
  * Not exposed to users - viewMode button behavior is fixed
2997
3133
  */
2998
3134
  toggleViewModeDropdown(button) {
2999
- const existingDropdown = document.querySelector(".overtype-dropdown-menu");
3000
- if (existingDropdown) {
3001
- existingDropdown.remove();
3002
- button.classList.remove("dropdown-active");
3135
+ if (this.activeDropdown) {
3136
+ this.closeViewModeDropdown(button);
3003
3137
  return;
3004
3138
  }
3139
+ this.openViewModeDropdown(button);
3140
+ }
3141
+ /**
3142
+ * Open the view mode dropdown
3143
+ */
3144
+ openViewModeDropdown(button, focusPlacement = null) {
3145
+ this.closeViewModeDropdown(button);
3005
3146
  button.classList.add("dropdown-active");
3006
3147
  const dropdown = this.createViewModeDropdown(button);
3007
3148
  const rect = button.getBoundingClientRect();
@@ -3009,16 +3150,42 @@ var Toolbar = class {
3009
3150
  dropdown.style.top = `${rect.bottom + 5}px`;
3010
3151
  dropdown.style.left = `${rect.left}px`;
3011
3152
  document.body.appendChild(dropdown);
3153
+ this.activeDropdown = dropdown;
3154
+ this.activeDropdownButton = button;
3155
+ button.setAttribute("aria-controls", dropdown.id);
3156
+ button.setAttribute("aria-expanded", "true");
3012
3157
  this.handleDocumentClick = (e) => {
3013
3158
  if (!dropdown.contains(e.target) && !button.contains(e.target)) {
3014
- dropdown.remove();
3015
- button.classList.remove("dropdown-active");
3016
- document.removeEventListener("click", this.handleDocumentClick);
3159
+ this.closeViewModeDropdown(button);
3017
3160
  }
3018
3161
  };
3019
3162
  setTimeout(() => {
3020
3163
  document.addEventListener("click", this.handleDocumentClick);
3021
3164
  }, 0);
3165
+ if (focusPlacement) {
3166
+ this.focusViewModeMenuItem(dropdown, focusPlacement);
3167
+ }
3168
+ }
3169
+ /**
3170
+ * Close the view mode dropdown
3171
+ */
3172
+ closeViewModeDropdown(button = this.activeDropdownButton, returnFocus = false) {
3173
+ if (this.activeDropdown) {
3174
+ this.activeDropdown.remove();
3175
+ this.activeDropdown = null;
3176
+ }
3177
+ if (button) {
3178
+ button.classList.remove("dropdown-active");
3179
+ button.setAttribute("aria-expanded", "false");
3180
+ }
3181
+ if (this.handleDocumentClick) {
3182
+ document.removeEventListener("click", this.handleDocumentClick);
3183
+ this.handleDocumentClick = null;
3184
+ }
3185
+ this.activeDropdownButton = null;
3186
+ if (returnFocus && button) {
3187
+ button.focus();
3188
+ }
3022
3189
  }
3023
3190
  /**
3024
3191
  * Create view mode dropdown menu (internal implementation)
@@ -3026,6 +3193,12 @@ var Toolbar = class {
3026
3193
  createViewModeDropdown(button) {
3027
3194
  const dropdown = document.createElement("div");
3028
3195
  dropdown.className = "overtype-dropdown-menu";
3196
+ dropdown.id = this.getInstanceElementId("toolbar-view-mode-menu");
3197
+ dropdown.setAttribute("role", "menu");
3198
+ dropdown.setAttribute("aria-label", "View mode");
3199
+ dropdown.addEventListener("keydown", (e) => {
3200
+ this.onViewModeMenuKeydown(e, button);
3201
+ });
3029
3202
  const items = [
3030
3203
  { id: "normal", label: "Normal Edit", icon: "\u2713" },
3031
3204
  { id: "plain", label: "Plain Textarea", icon: "\u2713" },
@@ -3036,12 +3209,15 @@ var Toolbar = class {
3036
3209
  const menuItem = document.createElement("button");
3037
3210
  menuItem.className = "overtype-dropdown-item";
3038
3211
  menuItem.type = "button";
3212
+ menuItem.tabIndex = -1;
3213
+ menuItem.setAttribute("role", "menuitemradio");
3214
+ menuItem.setAttribute("aria-checked", String(item.id === currentMode));
3039
3215
  menuItem.textContent = item.label;
3040
3216
  if (item.id === currentMode) {
3041
3217
  menuItem.classList.add("active");
3042
- menuItem.setAttribute("aria-current", "true");
3043
3218
  const checkmark = document.createElement("span");
3044
3219
  checkmark.className = "overtype-dropdown-icon";
3220
+ checkmark.setAttribute("aria-hidden", "true");
3045
3221
  checkmark.textContent = item.icon;
3046
3222
  menuItem.prepend(checkmark);
3047
3223
  }
@@ -3059,14 +3235,77 @@ var Toolbar = class {
3059
3235
  this.editor.showNormalEditMode();
3060
3236
  break;
3061
3237
  }
3062
- dropdown.remove();
3063
- button.classList.remove("dropdown-active");
3064
- document.removeEventListener("click", this.handleDocumentClick);
3238
+ this.closeViewModeDropdown(button, true);
3065
3239
  });
3066
3240
  dropdown.appendChild(menuItem);
3067
3241
  });
3068
3242
  return dropdown;
3069
3243
  }
3244
+ /**
3245
+ * Handle keyboard navigation inside the view mode menu
3246
+ */
3247
+ onViewModeMenuKeydown(e, button) {
3248
+ const menuItems = this.getViewModeMenuItems();
3249
+ const currentIndex = menuItems.indexOf(e.target);
3250
+ if (currentIndex === -1) {
3251
+ return;
3252
+ }
3253
+ switch (e.key) {
3254
+ case "ArrowDown":
3255
+ e.preventDefault();
3256
+ this.focusViewModeMenuItem(this.activeDropdown, currentIndex + 1);
3257
+ break;
3258
+ case "ArrowUp":
3259
+ e.preventDefault();
3260
+ this.focusViewModeMenuItem(this.activeDropdown, currentIndex - 1);
3261
+ break;
3262
+ case "Home":
3263
+ e.preventDefault();
3264
+ this.focusViewModeMenuItem(this.activeDropdown, "first");
3265
+ break;
3266
+ case "End":
3267
+ e.preventDefault();
3268
+ this.focusViewModeMenuItem(this.activeDropdown, "last");
3269
+ break;
3270
+ case "Escape":
3271
+ e.preventDefault();
3272
+ this.closeViewModeDropdown(button, true);
3273
+ break;
3274
+ }
3275
+ }
3276
+ /**
3277
+ * Focus a view mode menu item by index or placement
3278
+ */
3279
+ focusViewModeMenuItem(dropdown, placement) {
3280
+ const menuItems = this.getViewModeMenuItems(dropdown);
3281
+ if (menuItems.length === 0) {
3282
+ return;
3283
+ }
3284
+ let index = placement;
3285
+ if (placement === "first") {
3286
+ index = 0;
3287
+ } else if (placement === "last") {
3288
+ index = menuItems.length - 1;
3289
+ } else if (placement === "current") {
3290
+ index = menuItems.findIndex((item) => item.getAttribute("aria-checked") === "true");
3291
+ }
3292
+ if (index < 0) {
3293
+ index = menuItems.length - 1;
3294
+ }
3295
+ if (index >= menuItems.length) {
3296
+ index = 0;
3297
+ }
3298
+ menuItems[index].focus();
3299
+ }
3300
+ /**
3301
+ * Get the current view mode menu items
3302
+ */
3303
+ getViewModeMenuItems(dropdown = this.activeDropdown) {
3304
+ if (!dropdown) {
3305
+ return [];
3306
+ }
3307
+ return Array.from(dropdown.querySelectorAll('[role="menuitemradio"]'));
3308
+ }
3070
3309
  /**
3071
3310
  * Update active states of toolbar buttons
3072
3311
  */
@@ -3080,39 +3319,14 @@ var Toolbar = class {
3080
3319
  Object.entries(this.buttons).forEach(([name, button]) => {
3081
3320
  if (name === "viewMode")
3082
3321
  return;
3083
- let isActive = false;
3084
- switch (name) {
3085
- case "bold":
3086
- isActive = activeFormats.includes("bold");
3087
- break;
3088
- case "italic":
3089
- isActive = activeFormats.includes("italic");
3090
- break;
3091
- case "code":
3092
- isActive = false;
3093
- break;
3094
- case "bulletList":
3095
- isActive = activeFormats.includes("bullet-list");
3096
- break;
3097
- case "orderedList":
3098
- isActive = activeFormats.includes("numbered-list");
3099
- break;
3100
- case "taskList":
3101
- isActive = activeFormats.includes("task-list");
3102
- break;
3103
- case "quote":
3104
- isActive = activeFormats.includes("quote");
3105
- break;
3106
- case "h1":
3107
- isActive = activeFormats.includes("header");
3108
- break;
3109
- case "h2":
3110
- isActive = activeFormats.includes("header-2");
3111
- break;
3112
- case "h3":
3113
- isActive = activeFormats.includes("header-3");
3114
- break;
3322
+ const buttonConfig = this.toolbarButtons.find((buttonConfig2) => buttonConfig2.name === name);
3323
+ if (!(buttonConfig == null ? void 0 : buttonConfig.isActive)) {
3324
+ return;
3115
3325
  }
3326
+ const isActive = Boolean(buttonConfig.isActive({
3327
+ editor: this.editor,
3328
+ activeFormats
3329
+ }));
3116
3330
  button.classList.toggle("active", isActive);
3117
3331
  button.setAttribute("aria-pressed", isActive.toString());
3118
3332
  });
@@ -3134,14 +3348,21 @@ var Toolbar = class {
3134
3348
  */
3135
3349
  destroy() {
3136
3350
  if (this.container) {
3137
- if (this.handleDocumentClick) {
3351
+ if (this.activeDropdown) {
3352
+ this.closeViewModeDropdown();
3353
+ } else if (this.handleDocumentClick) {
3138
3354
  document.removeEventListener("click", this.handleDocumentClick);
3355
+ this.handleDocumentClick = null;
3139
3356
  }
3140
3357
  Object.values(this.buttons).forEach((button) => {
3141
3358
  if (button._clickHandler) {
3142
3359
  button.removeEventListener("click", button._clickHandler);
3143
3360
  delete button._clickHandler;
3144
3361
  }
3362
+ if (button._keydownHandler) {
3363
+ button.removeEventListener("keydown", button._keydownHandler);
3364
+ delete button._keydownHandler;
3365
+ }
3145
3366
  });
3146
3367
  this.container.remove();
3147
3368
  this.container = null;
@@ -4408,7 +4629,10 @@ var LinkTooltip = class {
4408
4629
  e.preventDefault();
4409
4630
  e.stopPropagation();
4410
4631
  if (this.currentLink) {
4411
- window.open(this.currentLink.url, "_blank");
4632
+ const safeUrl = MarkdownParser.sanitizeUrl(this.currentLink.url);
4633
+ if (safeUrl !== "#") {
4634
+ window.open(safeUrl, "_blank");
4635
+ }
4412
4636
  this.hide();
4413
4637
  }
4414
4638
  });
@@ -4436,7 +4660,7 @@ var LinkTooltip = class {
4436
4660
  if (position >= start && position <= end) {
4437
4661
  return {
4438
4662
  text: match[1],
4439
- url: match[2],
4663
+ url: this.transformUrl(match[2]),
4440
4664
  index: linkIndex,
4441
4665
  start,
4442
4666
  end
@@ -4446,6 +4670,18 @@ var LinkTooltip = class {
4446
4670
  }
4447
4671
  return null;
4448
4672
  }
4673
+ transformUrl(url) {
4674
+ const transform = this.editor.options.transformLinkUrl;
4675
+ if (typeof transform !== "function")
4676
+ return url;
4677
+ try {
4678
+ const result = transform(url);
4679
+ return typeof result === "string" ? result : url;
4680
+ } catch (e) {
4681
+ console.warn("transformLinkUrl threw:", e);
4682
+ return url;
4683
+ }
4684
+ }
4449
4685
  async show(linkInfo) {
4450
4686
  this.currentLink = linkInfo;
4451
4687
  this.cancelHide();
@@ -4597,6 +4833,7 @@ var toolbarButtons = {
4597
4833
  actionId: "toggleBold",
4598
4834
  icon: boldIcon,
4599
4835
  title: "Bold (Ctrl+B)",
4836
+ isActive: ({ activeFormats }) => activeFormats.includes("bold"),
4600
4837
  action: ({ editor }) => {
4601
4838
  toggleBold(editor.textarea);
4602
4839
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4607,6 +4844,7 @@ var toolbarButtons = {
4607
4844
  actionId: "toggleItalic",
4608
4845
  icon: italicIcon,
4609
4846
  title: "Italic (Ctrl+I)",
4847
+ isActive: ({ activeFormats }) => activeFormats.includes("italic"),
4610
4848
  action: ({ editor }) => {
4611
4849
  toggleItalic(editor.textarea);
4612
4850
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4617,6 +4855,7 @@ var toolbarButtons = {
4617
4855
  actionId: "toggleCode",
4618
4856
  icon: codeIcon,
4619
4857
  title: "Inline Code",
4858
+ isActive: () => false,
4620
4859
  action: ({ editor }) => {
4621
4860
  toggleCode(editor.textarea);
4622
4861
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4641,6 +4880,7 @@ var toolbarButtons = {
4641
4880
  actionId: "toggleH1",
4642
4881
  icon: h1Icon,
4643
4882
  title: "Heading 1",
4883
+ isActive: ({ activeFormats }) => activeFormats.includes("header"),
4644
4884
  action: ({ editor }) => {
4645
4885
  toggleH1(editor.textarea);
4646
4886
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4651,6 +4891,7 @@ var toolbarButtons = {
4651
4891
  actionId: "toggleH2",
4652
4892
  icon: h2Icon,
4653
4893
  title: "Heading 2",
4894
+ isActive: ({ activeFormats }) => activeFormats.includes("header-2"),
4654
4895
  action: ({ editor }) => {
4655
4896
  toggleH2(editor.textarea);
4656
4897
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4661,6 +4902,7 @@ var toolbarButtons = {
4661
4902
  actionId: "toggleH3",
4662
4903
  icon: h3Icon,
4663
4904
  title: "Heading 3",
4905
+ isActive: ({ activeFormats }) => activeFormats.includes("header-3"),
4664
4906
  action: ({ editor }) => {
4665
4907
  toggleH3(editor.textarea);
4666
4908
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4671,6 +4913,7 @@ var toolbarButtons = {
4671
4913
  actionId: "toggleBulletList",
4672
4914
  icon: bulletListIcon,
4673
4915
  title: "Bullet List",
4916
+ isActive: ({ activeFormats }) => activeFormats.includes("bullet-list"),
4674
4917
  action: ({ editor }) => {
4675
4918
  toggleBulletList(editor.textarea);
4676
4919
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4681,6 +4924,7 @@ var toolbarButtons = {
4681
4924
  actionId: "toggleNumberedList",
4682
4925
  icon: orderedListIcon,
4683
4926
  title: "Numbered List",
4927
+ isActive: ({ activeFormats }) => activeFormats.includes("numbered-list"),
4684
4928
  action: ({ editor }) => {
4685
4929
  toggleNumberedList(editor.textarea);
4686
4930
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4691,6 +4935,7 @@ var toolbarButtons = {
4691
4935
  actionId: "toggleTaskList",
4692
4936
  icon: taskListIcon,
4693
4937
  title: "Task List",
4938
+ isActive: ({ activeFormats }) => activeFormats.includes("task-list"),
4694
4939
  action: ({ editor }) => {
4695
4940
  if (toggleTaskList) {
4696
4941
  toggleTaskList(editor.textarea);
@@ -4703,6 +4948,7 @@ var toolbarButtons = {
4703
4948
  actionId: "toggleQuote",
4704
4949
  icon: quoteIcon,
4705
4950
  title: "Quote",
4951
+ isActive: ({ activeFormats }) => activeFormats.includes("quote"),
4706
4952
  action: ({ editor }) => {
4707
4953
  toggleQuote(editor.textarea);
4708
4954
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4764,6 +5010,17 @@ var defaultToolbarButtons = [
4764
5010
  ];
4765
5011
 
4766
5012
  // src/overtype.js
5013
+ var _isSafariCache;
5014
+ function isSafariBrowser() {
5015
+ if (_isSafariCache !== void 0)
5016
+ return _isSafariCache;
5017
+ _isSafariCache = false;
5018
+ if (typeof navigator !== "undefined") {
5019
+ const ua = navigator.userAgent || "";
5020
+ _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);
5021
+ }
5022
+ return _isSafariCache;
5023
+ }
4767
5024
  function buildActionsMap(buttons) {
4768
5025
  const map = {};
4769
5026
  (buttons || []).forEach((btn) => {
@@ -4838,6 +5095,8 @@ var _OverType = class _OverType {
4838
5095
  this.options = this._mergeOptions(options);
4839
5096
  this.instanceId = ++_OverType.instanceCount;
4840
5097
  this.initialized = false;
5098
+ this._isSafari = isSafariBrowser();
5099
+ this._safariReflowRaf = null;
4841
5100
  _OverType.injectStyles();
4842
5101
  _OverType.initGlobalListeners();
4843
5102
  const container = element.querySelector(".overtype-container");
@@ -4912,8 +5171,10 @@ var _OverType = class _OverType {
4912
5171
  // Enable smart list continuation
4913
5172
  codeHighlighter: null,
4914
5173
  // Per-instance code highlighter
4915
- spellcheck: false
5174
+ spellcheck: false,
4916
5175
  // Browser spellcheck (disabled by default)
5176
+ transformLinkUrl: null
5177
+ // Transform URLs shown/opened in the link tooltip
4917
5178
  };
4918
5179
  const { theme, colors, ...cleanOptions } = options;
4919
5180
  return {
@@ -4966,6 +5227,8 @@ var _OverType = class _OverType {
4966
5227
  this.wrapper._instance = this;
4967
5228
  this._applyInstanceCSSVars();
4968
5229
  this._configureTextarea();
5230
+ this._ensureTextareaId();
5231
+ this._syncPreviewInteractivity();
4969
5232
  this._applyOptions();
4970
5233
  }
4971
5234
  /**
@@ -5029,6 +5292,7 @@ var _OverType = class _OverType {
5029
5292
  }
5030
5293
  });
5031
5294
  }
5295
+ this._ensureTextareaId();
5032
5296
  this.preview = document.createElement("div");
5033
5297
  this.preview.className = "overtype-preview";
5034
5298
  this.preview.setAttribute("aria-hidden", "true");
@@ -5052,6 +5316,7 @@ var _OverType = class _OverType {
5052
5316
  } else {
5053
5317
  this.container.classList.remove("overtype-auto-resize");
5054
5318
  }
5319
+ this._syncPreviewInteractivity();
5055
5320
  }
5056
5321
  /**
5057
5322
  * Configure textarea attributes
@@ -5066,6 +5331,31 @@ var _OverType = class _OverType {
5066
5331
  this.textarea.setAttribute("data-gramm_editor", "false");
5067
5332
  this.textarea.setAttribute("data-enable-grammarly", "false");
5068
5333
  }
5334
+ /**
5335
+ * Ensure the textarea can be referenced by aria-controls
5336
+ * @private
5337
+ */
5338
+ _ensureTextareaId() {
5339
+ if (!this.textarea.id) {
5340
+ this.textarea.id = `overtype-${this.instanceId}-input`;
5341
+ }
5342
+ }
5343
+ /**
5344
+ * Keep rendered preview content out of keyboard navigation until Preview mode.
5345
+ * @private
5346
+ */
5347
+ _syncPreviewInteractivity() {
5348
+ if (!this.preview || !this.container)
5349
+ return;
5350
+ const isPreviewMode = this.container.dataset.mode === "preview";
5351
+ this.preview.inert = !isPreviewMode;
5352
+ this.preview.toggleAttribute("inert", !isPreviewMode);
5353
+ if (isPreviewMode) {
5354
+ this.preview.removeAttribute("aria-hidden");
5355
+ return;
5356
+ }
5357
+ this.preview.setAttribute("aria-hidden", "true");
5358
+ }
5069
5359
  /**
5070
5360
  * Create and setup toolbar
5071
5361
  * @private
@@ -5195,6 +5485,7 @@ var _OverType = class _OverType {
5195
5485
  return;
5196
5486
  }
5197
5487
  this._fileUploadCounter = 0;
5488
+ this._uploadedFiles = /* @__PURE__ */ new Map();
5198
5489
  this._boundHandleFilePaste = this._handleFilePaste.bind(this);
5199
5490
  this._boundHandleFileDrop = this._handleFileDrop.bind(this);
5200
5491
  this._boundHandleDragOver = this._handleDragOver.bind(this);
@@ -5203,6 +5494,53 @@ var _OverType = class _OverType {
5203
5494
  this.textarea.addEventListener("dragover", this._boundHandleDragOver);
5204
5495
  this.fileUploadInitialized = true;
5205
5496
  }
5497
+ /**
5498
+ * Extract URLs from markdown link syntax: [text](url) or ![text](url).
5499
+ * @private
5500
+ */
5501
+ _extractMarkdownUrls(text) {
5502
+ const urls = [];
5503
+ const re = /!?\[[^\]]*\]\(([^)\s]+)/g;
5504
+ let m;
5505
+ while ((m = re.exec(text)) !== null)
5506
+ urls.push(m[1]);
5507
+ return urls;
5508
+ }
5509
+ /**
5510
+ * Track URLs that were just inserted, pairing each with the source File.
5511
+ * If multiple URLs appear in one inserted block, all get associated with
5512
+ * the same file (rare; happens if onInsertFile returns several links).
5513
+ * @private
5514
+ */
5515
+ _trackInsertedUrls(insertedText, file) {
5516
+ if (!this._uploadedFiles || !file || !insertedText)
5517
+ return;
5518
+ for (const url of this._extractMarkdownUrls(insertedText)) {
5519
+ this._uploadedFiles.set(url, { filename: file.name, file });
5520
+ }
5521
+ }
5522
+ /**
5523
+ * Diff the tracked-URL set against the current value and fire
5524
+ * fileUpload.onRemoveFile for any URL no longer present.
5525
+ * @private
5526
+ */
5527
+ _checkForRemovedUploads() {
5528
+ var _a;
5529
+ if (!this._uploadedFiles || this._uploadedFiles.size === 0)
5530
+ return;
5531
+ const cb = (_a = this.options.fileUpload) == null ? void 0 : _a.onRemoveFile;
5532
+ const value = this.textarea.value;
5533
+ const removed = [];
5534
+ for (const [url, info] of this._uploadedFiles) {
5535
+ if (!value.includes(url))
5536
+ removed.push({ url, info });
5537
+ }
5538
+ for (const { url, info } of removed) {
5539
+ this._uploadedFiles.delete(url);
5540
+ if (cb)
5541
+ cb({ url, filename: info.filename, file: info.file });
5542
+ }
5543
+ }
5206
5544
  _handleFilePaste(e) {
5207
5545
  var _a, _b;
5208
5546
  if (!((_b = (_a = e == null ? void 0 : e.clipboardData) == null ? void 0 : _a.files) == null ? void 0 : _b.length))
@@ -5235,6 +5573,7 @@ var _OverType = class _OverType {
5235
5573
  }
5236
5574
  this.options.fileUpload.onInsertFile(file).then((text) => {
5237
5575
  this.textarea.value = this.textarea.value.replace(placeholder, text);
5576
+ this._trackInsertedUrls(text, file);
5238
5577
  this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5239
5578
  }, (error) => {
5240
5579
  console.error("OverType: File upload failed", error);
@@ -5247,6 +5586,7 @@ var _OverType = class _OverType {
5247
5586
  const texts = Array.isArray(result) ? result : [result];
5248
5587
  texts.forEach((text, index) => {
5249
5588
  this.textarea.value = this.textarea.value.replace(files[index].placeholder, text);
5589
+ this._trackInsertedUrls(text, files[index].file);
5250
5590
  });
5251
5591
  this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5252
5592
  }, (error) => {
@@ -5268,6 +5608,7 @@ var _OverType = class _OverType {
5268
5608
  this._boundHandleFilePaste = null;
5269
5609
  this._boundHandleFileDrop = null;
5270
5610
  this._boundHandleDragOver = null;
5611
+ this._uploadedFiles = null;
5271
5612
  this.fileUploadInitialized = false;
5272
5613
  }
5273
5614
  insertAtCursor(text) {
@@ -5312,9 +5653,12 @@ var _OverType = class _OverType {
5312
5653
  * @private
5313
5654
  */
5314
5655
  _notifyChange() {
5315
- if (!this.options.onChange || !this.initialized)
5656
+ if (!this.initialized)
5316
5657
  return;
5317
- this.options.onChange(this.textarea.value, this);
5658
+ this._checkForRemovedUploads();
5659
+ if (this.options.onChange) {
5660
+ this.options.onChange(this.textarea.value, this);
5661
+ }
5318
5662
  }
5319
5663
  /**
5320
5664
  * Apply background styling to code blocks
@@ -5350,6 +5694,29 @@ var _OverType = class _OverType {
5350
5694
  handleInput(event) {
5351
5695
  this.updatePreview();
5352
5696
  this._notifyChange();
5697
+ this._scheduleSafariReflow();
5698
+ }
5699
+ /**
5700
+ * Force Safari to re-shape stale textarea text after an edit.
5701
+ * Safari can leave a textarea's glyph layout cached after incremental edits,
5702
+ * desyncing the caret/wrap from the styled preview overlay. Toggling
5703
+ * letter-spacing (with !important to beat the stylesheet rule) and reading
5704
+ * offsetHeight forces a synchronous re-shape. Safari-only, coalesced to one
5705
+ * run per animation frame.
5706
+ * @private
5707
+ */
5708
+ _scheduleSafariReflow() {
5709
+ if (!this._isSafari || this._safariReflowRaf)
5710
+ return;
5711
+ this._safariReflowRaf = requestAnimationFrame(() => {
5712
+ this._safariReflowRaf = null;
5713
+ const ta = this.textarea;
5714
+ if (!ta)
5715
+ return;
5716
+ ta.style.setProperty("letter-spacing", "-0.001px", "important");
5717
+ void ta.offsetHeight;
5718
+ ta.style.removeProperty("letter-spacing");
5719
+ });
5353
5720
  }
5354
5721
  /**
5355
5722
  * Handle focus events
@@ -5377,49 +5744,11 @@ var _OverType = class _OverType {
5377
5744
  if (event.key === "Tab") {
5378
5745
  const start = this.textarea.selectionStart;
5379
5746
  const end = this.textarea.selectionEnd;
5380
- const value = this.textarea.value;
5381
- if (event.shiftKey && start === end) {
5747
+ if (start !== end && this._canEditTextarea()) {
5748
+ event.preventDefault();
5749
+ event.shiftKey ? this.outdentSelection() : this.indentSelection();
5382
5750
  return;
5383
5751
  }
5384
- event.preventDefault();
5385
- if (start !== end && event.shiftKey) {
5386
- const before = value.substring(0, start);
5387
- const selection = value.substring(start, end);
5388
- const after = value.substring(end);
5389
- const lines = selection.split("\n");
5390
- const outdented = lines.map((line) => line.replace(/^ /, "")).join("\n");
5391
- if (document.execCommand) {
5392
- this.textarea.setSelectionRange(start, end);
5393
- document.execCommand("insertText", false, outdented);
5394
- } else {
5395
- this.textarea.value = before + outdented + after;
5396
- this.textarea.selectionStart = start;
5397
- this.textarea.selectionEnd = start + outdented.length;
5398
- }
5399
- } else if (start !== end) {
5400
- const before = value.substring(0, start);
5401
- const selection = value.substring(start, end);
5402
- const after = value.substring(end);
5403
- const lines = selection.split("\n");
5404
- const indented = lines.map((line) => " " + line).join("\n");
5405
- if (document.execCommand) {
5406
- this.textarea.setSelectionRange(start, end);
5407
- document.execCommand("insertText", false, indented);
5408
- } else {
5409
- this.textarea.value = before + indented + after;
5410
- this.textarea.selectionStart = start;
5411
- this.textarea.selectionEnd = start + indented.length;
5412
- }
5413
- } else {
5414
- if (document.execCommand) {
5415
- document.execCommand("insertText", false, " ");
5416
- } else {
5417
- this.textarea.value = value.substring(0, start) + " " + value.substring(end);
5418
- this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
5419
- }
5420
- }
5421
- this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5422
- return;
5423
5752
  }
5424
5753
  if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
5425
5754
  if (this.handleSmartListContinuation()) {
@@ -5556,6 +5885,7 @@ var _OverType = class _OverType {
5556
5885
  }
5557
5886
  if (didChange) {
5558
5887
  this._notifyChange();
5888
+ this._scheduleSafariReflow();
5559
5889
  }
5560
5890
  }
5561
5891
  /**
@@ -5616,6 +5946,63 @@ var _OverType = class _OverType {
5616
5946
  getPreviewHTML() {
5617
5947
  return this.preview.innerHTML;
5618
5948
  }
5949
+ /**
5950
+ * Indent the current line or selected lines by two spaces.
5951
+ */
5952
+ indentSelection() {
5953
+ this._replaceSelectedLines((line) => ` ${line}`);
5954
+ }
5955
+ /**
5956
+ * Outdent the current line or selected lines by up to two spaces or one tab.
5957
+ */
5958
+ outdentSelection() {
5959
+ this._replaceSelectedLines((line) => line.replace(/^( {1,2}|\t)/, ""));
5960
+ }
5961
+ /**
5962
+ * Replace full lines touched by the current selection.
5963
+ * @private
5964
+ */
5965
+ _replaceSelectedLines(transformLine) {
5966
+ if (!this._canEditTextarea())
5967
+ return false;
5968
+ const textarea = this.textarea;
5969
+ const { selectionStart, selectionEnd, value } = textarea;
5970
+ const lineStart = value.lastIndexOf("\n", selectionStart - 1) + 1;
5971
+ const effectiveEnd = this._effectiveSelectionEnd(value, selectionStart, selectionEnd);
5972
+ const lineEndOffset = value.indexOf("\n", effectiveEnd);
5973
+ const lineEnd = lineEndOffset === -1 ? value.length : lineEndOffset;
5974
+ const selectedLines = value.slice(lineStart, lineEnd);
5975
+ const replacement = selectedLines.split("\n").map(transformLine).join("\n");
5976
+ if (replacement === selectedLines)
5977
+ return false;
5978
+ textarea.setSelectionRange(lineStart, lineEnd);
5979
+ let inserted = false;
5980
+ try {
5981
+ inserted = document.execCommand("insertText", false, replacement);
5982
+ } catch (_) {
5983
+ }
5984
+ if (!inserted) {
5985
+ textarea.setRangeText(replacement, lineStart, lineEnd, "preserve");
5986
+ }
5987
+ textarea.setSelectionRange(lineStart, lineStart + replacement.length);
5988
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
5989
+ return true;
5990
+ }
5991
+ /**
5992
+ * @private
5993
+ */
5994
+ _effectiveSelectionEnd(value, selectionStart, selectionEnd) {
5995
+ if (selectionEnd > selectionStart && value[selectionEnd - 1] === "\n") {
5996
+ return selectionEnd - 1;
5997
+ }
5998
+ return selectionEnd;
5999
+ }
6000
+ /**
6001
+ * @private
6002
+ */
6003
+ _canEditTextarea() {
6004
+ return this.textarea && !this.textarea.disabled && !this.textarea.readOnly;
6005
+ }
5619
6006
  /**
5620
6007
  * Get clean HTML without any OverType-specific markup
5621
6008
  * Useful for exporting to other formats or storage
@@ -5839,6 +6226,7 @@ var _OverType = class _OverType {
5839
6226
  */
5840
6227
  showNormalEditMode() {
5841
6228
  this.container.dataset.mode = "normal";
6229
+ this._syncPreviewInteractivity();
5842
6230
  this.updatePreview();
5843
6231
  this._updateAutoHeight();
5844
6232
  requestAnimationFrame(() => {
@@ -5853,6 +6241,7 @@ var _OverType = class _OverType {
5853
6241
  */
5854
6242
  showPlainTextarea() {
5855
6243
  this.container.dataset.mode = "plain";
6244
+ this._syncPreviewInteractivity();
5856
6245
  this._updateAutoHeight();
5857
6246
  if (this.toolbar) {
5858
6247
  const toggleBtn = this.container.querySelector('[data-action="toggle-plain"]');
@@ -5869,6 +6258,7 @@ var _OverType = class _OverType {
5869
6258
  */
5870
6259
  showPreviewMode() {
5871
6260
  this.container.dataset.mode = "preview";
6261
+ this._syncPreviewInteractivity();
5872
6262
  this.updatePreview();
5873
6263
  this._updateAutoHeight();
5874
6264
  return this;
@@ -5887,6 +6277,10 @@ var _OverType = class _OverType {
5887
6277
  if (this.shortcuts) {
5888
6278
  this.shortcuts.destroy();
5889
6279
  }
6280
+ if (this._safariReflowRaf) {
6281
+ cancelAnimationFrame(this._safariReflowRaf);
6282
+ this._safariReflowRaf = null;
6283
+ }
5890
6284
  if (this.wrapper) {
5891
6285
  const content = this.getValue();
5892
6286
  this.wrapper.remove();
@@ -5918,11 +6312,16 @@ var _OverType = class _OverType {
5918
6312
  return elements.map((el) => {
5919
6313
  const options = { ...defaults };
5920
6314
  for (const attr of el.attributes) {
5921
- if (attr.name.startsWith("data-ot-")) {
5922
- const kebab = attr.name.slice(8);
5923
- const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
5924
- options[key] = _OverType._parseDataValue(attr.value);
6315
+ if (!attr.name.startsWith("data-ot-"))
6316
+ continue;
6317
+ const kebab = attr.name.slice(8);
6318
+ if (kebab.startsWith("textarea-") && kebab !== "textarea-props") {
6319
+ const propKey = kebab.slice(9).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6320
+ options.textareaProps = { ...options.textareaProps || {}, [propKey]: _OverType._parseDataValue(attr.value) };
6321
+ continue;
5925
6322
  }
6323
+ const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6324
+ options[key] = _OverType._parseDataValue(attr.value);
5926
6325
  }
5927
6326
  return new _OverType(el, options)[0];
5928
6327
  });
@@ -5967,6 +6366,14 @@ var _OverType = class _OverType {
5967
6366
  return null;
5968
6367
  if (value !== "" && !isNaN(Number(value)))
5969
6368
  return Number(value);
6369
+ const trimmed = value.trim();
6370
+ if (trimmed[0] === "{" || trimmed[0] === "[") {
6371
+ try {
6372
+ return JSON.parse(trimmed);
6373
+ } catch (e) {
6374
+ return value;
6375
+ }
6376
+ }
5970
6377
  return value;
5971
6378
  }
5972
6379
  /**
@@ -6145,8 +6552,13 @@ var _OverType = class _OverType {
6145
6552
  * Initialize global event listeners
6146
6553
  */
6147
6554
  static initGlobalListeners() {
6148
- if (_OverType.globalListenersInitialized)
6555
+ const globalScope = typeof window !== "undefined" ? window : globalThis;
6556
+ const globalListenersKey = "__overtypeGlobalListenersInitialized";
6557
+ if (_OverType.globalListenersInitialized || globalScope[globalListenersKey]) {
6558
+ _OverType.globalListenersInitialized = true;
6149
6559
  return;
6560
+ }
6561
+ globalScope[globalListenersKey] = true;
6150
6562
  document.addEventListener("input", (e) => {
6151
6563
  if (e.target && e.target.classList && e.target.classList.contains("overtype-input")) {
6152
6564
  const wrapper = e.target.closest(".overtype-wrapper");