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
@@ -758,6 +758,16 @@ var ShortcutsManager = class {
758
758
  const modKey = isMac ? event.metaKey : event.ctrlKey;
759
759
  if (!modKey)
760
760
  return false;
761
+ if (event.key === "]") {
762
+ event.preventDefault();
763
+ this.editor.indentSelection();
764
+ return true;
765
+ }
766
+ if (event.key === "[") {
767
+ event.preventDefault();
768
+ this.editor.outdentSelection();
769
+ return true;
770
+ }
761
771
  let actionId = null;
762
772
  switch (event.key.toLowerCase()) {
763
773
  case "b":
@@ -2870,6 +2880,10 @@ var Toolbar = class {
2870
2880
  this.editor = editor;
2871
2881
  this.container = null;
2872
2882
  this.buttons = {};
2883
+ this.currentItemIndex = 0;
2884
+ this.handleDocumentClick = null;
2885
+ this.activeDropdown = null;
2886
+ this.activeDropdownButton = null;
2873
2887
  this.toolbarButtons = options.toolbarButtons || [];
2874
2888
  }
2875
2889
  /**
@@ -2878,8 +2892,10 @@ var Toolbar = class {
2878
2892
  create() {
2879
2893
  this.container = document.createElement("div");
2880
2894
  this.container.className = "overtype-toolbar";
2895
+ this.container.id = this.getInstanceElementId("toolbar");
2881
2896
  this.container.setAttribute("role", "toolbar");
2882
2897
  this.container.setAttribute("aria-label", "Formatting toolbar");
2898
+ this.container.setAttribute("aria-controls", this.editor.textarea.id);
2883
2899
  this.toolbarButtons.forEach((buttonConfig) => {
2884
2900
  if (buttonConfig.name === "separator") {
2885
2901
  const separator = this.createSeparator();
@@ -2890,8 +2906,116 @@ var Toolbar = class {
2890
2906
  this.container.appendChild(button);
2891
2907
  }
2892
2908
  });
2909
+ this.setupRovingTabIndex();
2910
+ this.updateButtonStates();
2893
2911
  this.editor.container.insertBefore(this.container, this.editor.wrapper);
2894
2912
  }
2913
+ /**
2914
+ * Build a stable id from the owning OverType instance
2915
+ */
2916
+ getInstanceElementId(name) {
2917
+ return `overtype-${this.editor.instanceId}-${name}`;
2918
+ }
2919
+ /**
2920
+ * Configure toolbar focus management per the ARIA toolbar pattern
2921
+ */
2922
+ setupRovingTabIndex() {
2923
+ const toolbarItems = this.getToolbarItems();
2924
+ if (toolbarItems.length === 0) {
2925
+ return;
2926
+ }
2927
+ this.currentItemIndex = this.getValidItemIndex(this.currentItemIndex);
2928
+ this.updateTabIndexes();
2929
+ this.container.addEventListener("keydown", (e) => {
2930
+ this.onToolbarKeydown(e);
2931
+ });
2932
+ this.container.addEventListener("focusin", (e) => {
2933
+ this.onToolbarFocusin(e);
2934
+ });
2935
+ }
2936
+ /**
2937
+ * Get toolbar buttons in DOM order for keyboard navigation
2938
+ */
2939
+ getToolbarItems() {
2940
+ if (!this.container) {
2941
+ return [];
2942
+ }
2943
+ return Array.from(this.container.querySelectorAll(".overtype-toolbar-button"));
2944
+ }
2945
+ /**
2946
+ * Handle keyboard navigation within the toolbar
2947
+ */
2948
+ onToolbarKeydown(e) {
2949
+ const toolbarItems = this.getToolbarItems();
2950
+ if (!toolbarItems.includes(e.target)) {
2951
+ return;
2952
+ }
2953
+ switch (e.key) {
2954
+ case "ArrowRight":
2955
+ e.preventDefault();
2956
+ this.focusItem(this.currentItemIndex + 1);
2957
+ break;
2958
+ case "ArrowLeft":
2959
+ e.preventDefault();
2960
+ this.focusItem(this.currentItemIndex - 1);
2961
+ break;
2962
+ case "Home":
2963
+ e.preventDefault();
2964
+ this.focusItem(0);
2965
+ break;
2966
+ case "End":
2967
+ e.preventDefault();
2968
+ this.focusItem(toolbarItems.length - 1);
2969
+ break;
2970
+ }
2971
+ }
2972
+ /**
2973
+ * Remember the focused toolbar item as the toolbar tab stop
2974
+ */
2975
+ onToolbarFocusin(e) {
2976
+ const focusedItemIndex = this.getToolbarItems().indexOf(e.target);
2977
+ if (focusedItemIndex === -1) {
2978
+ return;
2979
+ }
2980
+ this.currentItemIndex = focusedItemIndex;
2981
+ this.updateTabIndexes();
2982
+ }
2983
+ /**
2984
+ * Move focus to a toolbar item and make it the only tab stop
2985
+ */
2986
+ focusItem(index) {
2987
+ const toolbarItems = this.getToolbarItems();
2988
+ if (toolbarItems.length === 0) {
2989
+ return;
2990
+ }
2991
+ this.currentItemIndex = this.getValidItemIndex(index, toolbarItems);
2992
+ this.updateTabIndexes();
2993
+ toolbarItems[this.currentItemIndex].focus();
2994
+ }
2995
+ /**
2996
+ * Normalize toolbar item indexes with wrapping
2997
+ */
2998
+ getValidItemIndex(index, toolbarItems = this.getToolbarItems()) {
2999
+ const itemCount = toolbarItems.length;
3000
+ if (itemCount === 0) {
3001
+ return 0;
3002
+ }
3003
+ if (index < 0) {
3004
+ return itemCount - 1;
3005
+ }
3006
+ if (index >= itemCount) {
3007
+ return 0;
3008
+ }
3009
+ return index;
3010
+ }
3011
+ /**
3012
+ * Keep exactly one toolbar item in the page tab sequence
3013
+ */
3014
+ updateTabIndexes() {
3015
+ this.getToolbarItems().forEach((item, index) => {
3016
+ item.tabIndex = index === this.currentItemIndex ? 0 : -1;
3017
+ });
3018
+ }
2895
3019
  /**
2896
3020
  * Create a toolbar separator
2897
3021
  */
@@ -2915,10 +3039,22 @@ var Toolbar = class {
2915
3039
  if (buttonConfig.name === "viewMode") {
2916
3040
  button.classList.add("has-dropdown");
2917
3041
  button.dataset.dropdown = "true";
2918
- button.addEventListener("click", (e) => {
3042
+ button.setAttribute("aria-haspopup", "menu");
3043
+ button.setAttribute("aria-expanded", "false");
3044
+ button._clickHandler = (e) => {
2919
3045
  e.preventDefault();
2920
3046
  this.toggleViewModeDropdown(button);
2921
- });
3047
+ };
3048
+ button._keydownHandler = (e) => {
3049
+ if (!["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
3050
+ return;
3051
+ }
3052
+ e.preventDefault();
3053
+ const placement = e.key === "ArrowUp" ? "last" : "current";
3054
+ this.openViewModeDropdown(button, placement);
3055
+ };
3056
+ button.addEventListener("click", button._clickHandler);
3057
+ button.addEventListener("keydown", button._keydownHandler);
2922
3058
  return button;
2923
3059
  }
2924
3060
  button._clickHandler = (e) => {
@@ -2973,12 +3109,17 @@ var Toolbar = class {
2973
3109
  * Not exposed to users - viewMode button behavior is fixed
2974
3110
  */
2975
3111
  toggleViewModeDropdown(button) {
2976
- const existingDropdown = document.querySelector(".overtype-dropdown-menu");
2977
- if (existingDropdown) {
2978
- existingDropdown.remove();
2979
- button.classList.remove("dropdown-active");
3112
+ if (this.activeDropdown) {
3113
+ this.closeViewModeDropdown(button);
2980
3114
  return;
2981
3115
  }
3116
+ this.openViewModeDropdown(button);
3117
+ }
3118
+ /**
3119
+ * Open the view mode dropdown
3120
+ */
3121
+ openViewModeDropdown(button, focusPlacement = null) {
3122
+ this.closeViewModeDropdown(button);
2982
3123
  button.classList.add("dropdown-active");
2983
3124
  const dropdown = this.createViewModeDropdown(button);
2984
3125
  const rect = button.getBoundingClientRect();
@@ -2986,16 +3127,42 @@ var Toolbar = class {
2986
3127
  dropdown.style.top = `${rect.bottom + 5}px`;
2987
3128
  dropdown.style.left = `${rect.left}px`;
2988
3129
  document.body.appendChild(dropdown);
3130
+ this.activeDropdown = dropdown;
3131
+ this.activeDropdownButton = button;
3132
+ button.setAttribute("aria-controls", dropdown.id);
3133
+ button.setAttribute("aria-expanded", "true");
2989
3134
  this.handleDocumentClick = (e) => {
2990
3135
  if (!dropdown.contains(e.target) && !button.contains(e.target)) {
2991
- dropdown.remove();
2992
- button.classList.remove("dropdown-active");
2993
- document.removeEventListener("click", this.handleDocumentClick);
3136
+ this.closeViewModeDropdown(button);
2994
3137
  }
2995
3138
  };
2996
3139
  setTimeout(() => {
2997
3140
  document.addEventListener("click", this.handleDocumentClick);
2998
3141
  }, 0);
3142
+ if (focusPlacement) {
3143
+ this.focusViewModeMenuItem(dropdown, focusPlacement);
3144
+ }
3145
+ }
3146
+ /**
3147
+ * Close the view mode dropdown
3148
+ */
3149
+ closeViewModeDropdown(button = this.activeDropdownButton, returnFocus = false) {
3150
+ if (this.activeDropdown) {
3151
+ this.activeDropdown.remove();
3152
+ this.activeDropdown = null;
3153
+ }
3154
+ if (button) {
3155
+ button.classList.remove("dropdown-active");
3156
+ button.setAttribute("aria-expanded", "false");
3157
+ }
3158
+ if (this.handleDocumentClick) {
3159
+ document.removeEventListener("click", this.handleDocumentClick);
3160
+ this.handleDocumentClick = null;
3161
+ }
3162
+ this.activeDropdownButton = null;
3163
+ if (returnFocus && button) {
3164
+ button.focus();
3165
+ }
2999
3166
  }
3000
3167
  /**
3001
3168
  * Create view mode dropdown menu (internal implementation)
@@ -3003,6 +3170,12 @@ var Toolbar = class {
3003
3170
  createViewModeDropdown(button) {
3004
3171
  const dropdown = document.createElement("div");
3005
3172
  dropdown.className = "overtype-dropdown-menu";
3173
+ dropdown.id = this.getInstanceElementId("toolbar-view-mode-menu");
3174
+ dropdown.setAttribute("role", "menu");
3175
+ dropdown.setAttribute("aria-label", "View mode");
3176
+ dropdown.addEventListener("keydown", (e) => {
3177
+ this.onViewModeMenuKeydown(e, button);
3178
+ });
3006
3179
  const items = [
3007
3180
  { id: "normal", label: "Normal Edit", icon: "\u2713" },
3008
3181
  { id: "plain", label: "Plain Textarea", icon: "\u2713" },
@@ -3013,12 +3186,15 @@ var Toolbar = class {
3013
3186
  const menuItem = document.createElement("button");
3014
3187
  menuItem.className = "overtype-dropdown-item";
3015
3188
  menuItem.type = "button";
3189
+ menuItem.tabIndex = -1;
3190
+ menuItem.setAttribute("role", "menuitemradio");
3191
+ menuItem.setAttribute("aria-checked", String(item.id === currentMode));
3016
3192
  menuItem.textContent = item.label;
3017
3193
  if (item.id === currentMode) {
3018
3194
  menuItem.classList.add("active");
3019
- menuItem.setAttribute("aria-current", "true");
3020
3195
  const checkmark = document.createElement("span");
3021
3196
  checkmark.className = "overtype-dropdown-icon";
3197
+ checkmark.setAttribute("aria-hidden", "true");
3022
3198
  checkmark.textContent = item.icon;
3023
3199
  menuItem.prepend(checkmark);
3024
3200
  }
@@ -3036,14 +3212,77 @@ var Toolbar = class {
3036
3212
  this.editor.showNormalEditMode();
3037
3213
  break;
3038
3214
  }
3039
- dropdown.remove();
3040
- button.classList.remove("dropdown-active");
3041
- document.removeEventListener("click", this.handleDocumentClick);
3215
+ this.closeViewModeDropdown(button, true);
3042
3216
  });
3043
3217
  dropdown.appendChild(menuItem);
3044
3218
  });
3045
3219
  return dropdown;
3046
3220
  }
3221
+ /**
3222
+ * Handle keyboard navigation inside the view mode menu
3223
+ */
3224
+ onViewModeMenuKeydown(e, button) {
3225
+ const menuItems = this.getViewModeMenuItems();
3226
+ const currentIndex = menuItems.indexOf(e.target);
3227
+ if (currentIndex === -1) {
3228
+ return;
3229
+ }
3230
+ switch (e.key) {
3231
+ case "ArrowDown":
3232
+ e.preventDefault();
3233
+ this.focusViewModeMenuItem(this.activeDropdown, currentIndex + 1);
3234
+ break;
3235
+ case "ArrowUp":
3236
+ e.preventDefault();
3237
+ this.focusViewModeMenuItem(this.activeDropdown, currentIndex - 1);
3238
+ break;
3239
+ case "Home":
3240
+ e.preventDefault();
3241
+ this.focusViewModeMenuItem(this.activeDropdown, "first");
3242
+ break;
3243
+ case "End":
3244
+ e.preventDefault();
3245
+ this.focusViewModeMenuItem(this.activeDropdown, "last");
3246
+ break;
3247
+ case "Escape":
3248
+ e.preventDefault();
3249
+ this.closeViewModeDropdown(button, true);
3250
+ break;
3251
+ }
3252
+ }
3253
+ /**
3254
+ * Focus a view mode menu item by index or placement
3255
+ */
3256
+ focusViewModeMenuItem(dropdown, placement) {
3257
+ const menuItems = this.getViewModeMenuItems(dropdown);
3258
+ if (menuItems.length === 0) {
3259
+ return;
3260
+ }
3261
+ let index = placement;
3262
+ if (placement === "first") {
3263
+ index = 0;
3264
+ } else if (placement === "last") {
3265
+ index = menuItems.length - 1;
3266
+ } else if (placement === "current") {
3267
+ index = menuItems.findIndex((item) => item.getAttribute("aria-checked") === "true");
3268
+ }
3269
+ if (index < 0) {
3270
+ index = menuItems.length - 1;
3271
+ }
3272
+ if (index >= menuItems.length) {
3273
+ index = 0;
3274
+ }
3275
+ menuItems[index].focus();
3276
+ }
3277
+ /**
3278
+ * Get the current view mode menu items
3279
+ */
3280
+ getViewModeMenuItems(dropdown = this.activeDropdown) {
3281
+ if (!dropdown) {
3282
+ return [];
3283
+ }
3284
+ return Array.from(dropdown.querySelectorAll('[role="menuitemradio"]'));
3285
+ }
3047
3286
  /**
3048
3287
  * Update active states of toolbar buttons
3049
3288
  */
@@ -3057,39 +3296,14 @@ var Toolbar = class {
3057
3296
  Object.entries(this.buttons).forEach(([name, button]) => {
3058
3297
  if (name === "viewMode")
3059
3298
  return;
3060
- let isActive = false;
3061
- switch (name) {
3062
- case "bold":
3063
- isActive = activeFormats.includes("bold");
3064
- break;
3065
- case "italic":
3066
- isActive = activeFormats.includes("italic");
3067
- break;
3068
- case "code":
3069
- isActive = false;
3070
- break;
3071
- case "bulletList":
3072
- isActive = activeFormats.includes("bullet-list");
3073
- break;
3074
- case "orderedList":
3075
- isActive = activeFormats.includes("numbered-list");
3076
- break;
3077
- case "taskList":
3078
- isActive = activeFormats.includes("task-list");
3079
- break;
3080
- case "quote":
3081
- isActive = activeFormats.includes("quote");
3082
- break;
3083
- case "h1":
3084
- isActive = activeFormats.includes("header");
3085
- break;
3086
- case "h2":
3087
- isActive = activeFormats.includes("header-2");
3088
- break;
3089
- case "h3":
3090
- isActive = activeFormats.includes("header-3");
3091
- break;
3299
+ const buttonConfig = this.toolbarButtons.find((buttonConfig2) => buttonConfig2.name === name);
3300
+ if (!(buttonConfig == null ? void 0 : buttonConfig.isActive)) {
3301
+ return;
3092
3302
  }
3303
+ const isActive = Boolean(buttonConfig.isActive({
3304
+ editor: this.editor,
3305
+ activeFormats
3306
+ }));
3093
3307
  button.classList.toggle("active", isActive);
3094
3308
  button.setAttribute("aria-pressed", isActive.toString());
3095
3309
  });
@@ -3111,14 +3325,21 @@ var Toolbar = class {
3111
3325
  */
3112
3326
  destroy() {
3113
3327
  if (this.container) {
3114
- if (this.handleDocumentClick) {
3328
+ if (this.activeDropdown) {
3329
+ this.closeViewModeDropdown();
3330
+ } else if (this.handleDocumentClick) {
3115
3331
  document.removeEventListener("click", this.handleDocumentClick);
3332
+ this.handleDocumentClick = null;
3116
3333
  }
3117
3334
  Object.values(this.buttons).forEach((button) => {
3118
3335
  if (button._clickHandler) {
3119
3336
  button.removeEventListener("click", button._clickHandler);
3120
3337
  delete button._clickHandler;
3121
3338
  }
3339
+ if (button._keydownHandler) {
3340
+ button.removeEventListener("keydown", button._keydownHandler);
3341
+ delete button._keydownHandler;
3342
+ }
3122
3343
  });
3123
3344
  this.container.remove();
3124
3345
  this.container = null;
@@ -4385,7 +4606,10 @@ var LinkTooltip = class {
4385
4606
  e.preventDefault();
4386
4607
  e.stopPropagation();
4387
4608
  if (this.currentLink) {
4388
- window.open(this.currentLink.url, "_blank");
4609
+ const safeUrl = MarkdownParser.sanitizeUrl(this.currentLink.url);
4610
+ if (safeUrl !== "#") {
4611
+ window.open(safeUrl, "_blank");
4612
+ }
4389
4613
  this.hide();
4390
4614
  }
4391
4615
  });
@@ -4413,7 +4637,7 @@ var LinkTooltip = class {
4413
4637
  if (position >= start && position <= end) {
4414
4638
  return {
4415
4639
  text: match[1],
4416
- url: match[2],
4640
+ url: this.transformUrl(match[2]),
4417
4641
  index: linkIndex,
4418
4642
  start,
4419
4643
  end
@@ -4423,6 +4647,18 @@ var LinkTooltip = class {
4423
4647
  }
4424
4648
  return null;
4425
4649
  }
4650
+ transformUrl(url) {
4651
+ const transform = this.editor.options.transformLinkUrl;
4652
+ if (typeof transform !== "function")
4653
+ return url;
4654
+ try {
4655
+ const result = transform(url);
4656
+ return typeof result === "string" ? result : url;
4657
+ } catch (e) {
4658
+ console.warn("transformLinkUrl threw:", e);
4659
+ return url;
4660
+ }
4661
+ }
4426
4662
  async show(linkInfo) {
4427
4663
  this.currentLink = linkInfo;
4428
4664
  this.cancelHide();
@@ -4574,6 +4810,7 @@ var toolbarButtons = {
4574
4810
  actionId: "toggleBold",
4575
4811
  icon: boldIcon,
4576
4812
  title: "Bold (Ctrl+B)",
4813
+ isActive: ({ activeFormats }) => activeFormats.includes("bold"),
4577
4814
  action: ({ editor }) => {
4578
4815
  toggleBold(editor.textarea);
4579
4816
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4584,6 +4821,7 @@ var toolbarButtons = {
4584
4821
  actionId: "toggleItalic",
4585
4822
  icon: italicIcon,
4586
4823
  title: "Italic (Ctrl+I)",
4824
+ isActive: ({ activeFormats }) => activeFormats.includes("italic"),
4587
4825
  action: ({ editor }) => {
4588
4826
  toggleItalic(editor.textarea);
4589
4827
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4594,6 +4832,7 @@ var toolbarButtons = {
4594
4832
  actionId: "toggleCode",
4595
4833
  icon: codeIcon,
4596
4834
  title: "Inline Code",
4835
+ isActive: () => false,
4597
4836
  action: ({ editor }) => {
4598
4837
  toggleCode(editor.textarea);
4599
4838
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4618,6 +4857,7 @@ var toolbarButtons = {
4618
4857
  actionId: "toggleH1",
4619
4858
  icon: h1Icon,
4620
4859
  title: "Heading 1",
4860
+ isActive: ({ activeFormats }) => activeFormats.includes("header"),
4621
4861
  action: ({ editor }) => {
4622
4862
  toggleH1(editor.textarea);
4623
4863
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4628,6 +4868,7 @@ var toolbarButtons = {
4628
4868
  actionId: "toggleH2",
4629
4869
  icon: h2Icon,
4630
4870
  title: "Heading 2",
4871
+ isActive: ({ activeFormats }) => activeFormats.includes("header-2"),
4631
4872
  action: ({ editor }) => {
4632
4873
  toggleH2(editor.textarea);
4633
4874
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4638,6 +4879,7 @@ var toolbarButtons = {
4638
4879
  actionId: "toggleH3",
4639
4880
  icon: h3Icon,
4640
4881
  title: "Heading 3",
4882
+ isActive: ({ activeFormats }) => activeFormats.includes("header-3"),
4641
4883
  action: ({ editor }) => {
4642
4884
  toggleH3(editor.textarea);
4643
4885
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4648,6 +4890,7 @@ var toolbarButtons = {
4648
4890
  actionId: "toggleBulletList",
4649
4891
  icon: bulletListIcon,
4650
4892
  title: "Bullet List",
4893
+ isActive: ({ activeFormats }) => activeFormats.includes("bullet-list"),
4651
4894
  action: ({ editor }) => {
4652
4895
  toggleBulletList(editor.textarea);
4653
4896
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4658,6 +4901,7 @@ var toolbarButtons = {
4658
4901
  actionId: "toggleNumberedList",
4659
4902
  icon: orderedListIcon,
4660
4903
  title: "Numbered List",
4904
+ isActive: ({ activeFormats }) => activeFormats.includes("numbered-list"),
4661
4905
  action: ({ editor }) => {
4662
4906
  toggleNumberedList(editor.textarea);
4663
4907
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4668,6 +4912,7 @@ var toolbarButtons = {
4668
4912
  actionId: "toggleTaskList",
4669
4913
  icon: taskListIcon,
4670
4914
  title: "Task List",
4915
+ isActive: ({ activeFormats }) => activeFormats.includes("task-list"),
4671
4916
  action: ({ editor }) => {
4672
4917
  if (toggleTaskList) {
4673
4918
  toggleTaskList(editor.textarea);
@@ -4680,6 +4925,7 @@ var toolbarButtons = {
4680
4925
  actionId: "toggleQuote",
4681
4926
  icon: quoteIcon,
4682
4927
  title: "Quote",
4928
+ isActive: ({ activeFormats }) => activeFormats.includes("quote"),
4683
4929
  action: ({ editor }) => {
4684
4930
  toggleQuote(editor.textarea);
4685
4931
  editor.textarea.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4741,6 +4987,17 @@ var defaultToolbarButtons = [
4741
4987
  ];
4742
4988
 
4743
4989
  // src/overtype.js
4990
+ var _isSafariCache;
4991
+ function isSafariBrowser() {
4992
+ if (_isSafariCache !== void 0)
4993
+ return _isSafariCache;
4994
+ _isSafariCache = false;
4995
+ if (typeof navigator !== "undefined") {
4996
+ const ua = navigator.userAgent || "";
4997
+ _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);
4998
+ }
4999
+ return _isSafariCache;
5000
+ }
4744
5001
  function buildActionsMap(buttons) {
4745
5002
  const map = {};
4746
5003
  (buttons || []).forEach((btn) => {
@@ -4815,6 +5072,8 @@ var _OverType = class _OverType {
4815
5072
  this.options = this._mergeOptions(options);
4816
5073
  this.instanceId = ++_OverType.instanceCount;
4817
5074
  this.initialized = false;
5075
+ this._isSafari = isSafariBrowser();
5076
+ this._safariReflowRaf = null;
4818
5077
  _OverType.injectStyles();
4819
5078
  _OverType.initGlobalListeners();
4820
5079
  const container = element.querySelector(".overtype-container");
@@ -4889,8 +5148,10 @@ var _OverType = class _OverType {
4889
5148
  // Enable smart list continuation
4890
5149
  codeHighlighter: null,
4891
5150
  // Per-instance code highlighter
4892
- spellcheck: false
5151
+ spellcheck: false,
4893
5152
  // Browser spellcheck (disabled by default)
5153
+ transformLinkUrl: null
5154
+ // Transform URLs shown/opened in the link tooltip
4894
5155
  };
4895
5156
  const { theme, colors, ...cleanOptions } = options;
4896
5157
  return {
@@ -4943,6 +5204,8 @@ var _OverType = class _OverType {
4943
5204
  this.wrapper._instance = this;
4944
5205
  this._applyInstanceCSSVars();
4945
5206
  this._configureTextarea();
5207
+ this._ensureTextareaId();
5208
+ this._syncPreviewInteractivity();
4946
5209
  this._applyOptions();
4947
5210
  }
4948
5211
  /**
@@ -5006,6 +5269,7 @@ var _OverType = class _OverType {
5006
5269
  }
5007
5270
  });
5008
5271
  }
5272
+ this._ensureTextareaId();
5009
5273
  this.preview = document.createElement("div");
5010
5274
  this.preview.className = "overtype-preview";
5011
5275
  this.preview.setAttribute("aria-hidden", "true");
@@ -5029,6 +5293,7 @@ var _OverType = class _OverType {
5029
5293
  } else {
5030
5294
  this.container.classList.remove("overtype-auto-resize");
5031
5295
  }
5296
+ this._syncPreviewInteractivity();
5032
5297
  }
5033
5298
  /**
5034
5299
  * Configure textarea attributes
@@ -5043,6 +5308,31 @@ var _OverType = class _OverType {
5043
5308
  this.textarea.setAttribute("data-gramm_editor", "false");
5044
5309
  this.textarea.setAttribute("data-enable-grammarly", "false");
5045
5310
  }
5311
+ /**
5312
+ * Ensure the textarea can be referenced by aria-controls
5313
+ * @private
5314
+ */
5315
+ _ensureTextareaId() {
5316
+ if (!this.textarea.id) {
5317
+ this.textarea.id = `overtype-${this.instanceId}-input`;
5318
+ }
5319
+ }
5320
+ /**
5321
+ * Keep rendered preview content out of keyboard navigation until Preview mode.
5322
+ * @private
5323
+ */
5324
+ _syncPreviewInteractivity() {
5325
+ if (!this.preview || !this.container)
5326
+ return;
5327
+ const isPreviewMode = this.container.dataset.mode === "preview";
5328
+ this.preview.inert = !isPreviewMode;
5329
+ this.preview.toggleAttribute("inert", !isPreviewMode);
5330
+ if (isPreviewMode) {
5331
+ this.preview.removeAttribute("aria-hidden");
5332
+ return;
5333
+ }
5334
+ this.preview.setAttribute("aria-hidden", "true");
5335
+ }
5046
5336
  /**
5047
5337
  * Create and setup toolbar
5048
5338
  * @private
@@ -5381,6 +5671,29 @@ var _OverType = class _OverType {
5381
5671
  handleInput(event) {
5382
5672
  this.updatePreview();
5383
5673
  this._notifyChange();
5674
+ this._scheduleSafariReflow();
5675
+ }
5676
+ /**
5677
+ * Force Safari to re-shape stale textarea text after an edit.
5678
+ * Safari can leave a textarea's glyph layout cached after incremental edits,
5679
+ * desyncing the caret/wrap from the styled preview overlay. Toggling
5680
+ * letter-spacing (with !important to beat the stylesheet rule) and reading
5681
+ * offsetHeight forces a synchronous re-shape. Safari-only, coalesced to one
5682
+ * run per animation frame.
5683
+ * @private
5684
+ */
5685
+ _scheduleSafariReflow() {
5686
+ if (!this._isSafari || this._safariReflowRaf)
5687
+ return;
5688
+ this._safariReflowRaf = requestAnimationFrame(() => {
5689
+ this._safariReflowRaf = null;
5690
+ const ta = this.textarea;
5691
+ if (!ta)
5692
+ return;
5693
+ ta.style.setProperty("letter-spacing", "-0.001px", "important");
5694
+ void ta.offsetHeight;
5695
+ ta.style.removeProperty("letter-spacing");
5696
+ });
5384
5697
  }
5385
5698
  /**
5386
5699
  * Handle focus events
@@ -5408,49 +5721,11 @@ var _OverType = class _OverType {
5408
5721
  if (event.key === "Tab") {
5409
5722
  const start = this.textarea.selectionStart;
5410
5723
  const end = this.textarea.selectionEnd;
5411
- const value = this.textarea.value;
5412
- if (event.shiftKey && start === end) {
5724
+ if (start !== end && this._canEditTextarea()) {
5725
+ event.preventDefault();
5726
+ event.shiftKey ? this.outdentSelection() : this.indentSelection();
5413
5727
  return;
5414
5728
  }
5415
- event.preventDefault();
5416
- if (start !== end && event.shiftKey) {
5417
- const before = value.substring(0, start);
5418
- const selection = value.substring(start, end);
5419
- const after = value.substring(end);
5420
- const lines = selection.split("\n");
5421
- const outdented = lines.map((line) => line.replace(/^ /, "")).join("\n");
5422
- if (document.execCommand) {
5423
- this.textarea.setSelectionRange(start, end);
5424
- document.execCommand("insertText", false, outdented);
5425
- } else {
5426
- this.textarea.value = before + outdented + after;
5427
- this.textarea.selectionStart = start;
5428
- this.textarea.selectionEnd = start + outdented.length;
5429
- }
5430
- } else if (start !== end) {
5431
- const before = value.substring(0, start);
5432
- const selection = value.substring(start, end);
5433
- const after = value.substring(end);
5434
- const lines = selection.split("\n");
5435
- const indented = lines.map((line) => " " + line).join("\n");
5436
- if (document.execCommand) {
5437
- this.textarea.setSelectionRange(start, end);
5438
- document.execCommand("insertText", false, indented);
5439
- } else {
5440
- this.textarea.value = before + indented + after;
5441
- this.textarea.selectionStart = start;
5442
- this.textarea.selectionEnd = start + indented.length;
5443
- }
5444
- } else {
5445
- if (document.execCommand) {
5446
- document.execCommand("insertText", false, " ");
5447
- } else {
5448
- this.textarea.value = value.substring(0, start) + " " + value.substring(end);
5449
- this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
5450
- }
5451
- }
5452
- this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
5453
- return;
5454
5729
  }
5455
5730
  if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
5456
5731
  if (this.handleSmartListContinuation()) {
@@ -5587,6 +5862,7 @@ var _OverType = class _OverType {
5587
5862
  }
5588
5863
  if (didChange) {
5589
5864
  this._notifyChange();
5865
+ this._scheduleSafariReflow();
5590
5866
  }
5591
5867
  }
5592
5868
  /**
@@ -5647,6 +5923,63 @@ var _OverType = class _OverType {
5647
5923
  getPreviewHTML() {
5648
5924
  return this.preview.innerHTML;
5649
5925
  }
5926
+ /**
5927
+ * Indent the current line or selected lines by two spaces.
5928
+ */
5929
+ indentSelection() {
5930
+ this._replaceSelectedLines((line) => ` ${line}`);
5931
+ }
5932
+ /**
5933
+ * Outdent the current line or selected lines by up to two spaces or one tab.
5934
+ */
5935
+ outdentSelection() {
5936
+ this._replaceSelectedLines((line) => line.replace(/^( {1,2}|\t)/, ""));
5937
+ }
5938
+ /**
5939
+ * Replace full lines touched by the current selection.
5940
+ * @private
5941
+ */
5942
+ _replaceSelectedLines(transformLine) {
5943
+ if (!this._canEditTextarea())
5944
+ return false;
5945
+ const textarea = this.textarea;
5946
+ const { selectionStart, selectionEnd, value } = textarea;
5947
+ const lineStart = value.lastIndexOf("\n", selectionStart - 1) + 1;
5948
+ const effectiveEnd = this._effectiveSelectionEnd(value, selectionStart, selectionEnd);
5949
+ const lineEndOffset = value.indexOf("\n", effectiveEnd);
5950
+ const lineEnd = lineEndOffset === -1 ? value.length : lineEndOffset;
5951
+ const selectedLines = value.slice(lineStart, lineEnd);
5952
+ const replacement = selectedLines.split("\n").map(transformLine).join("\n");
5953
+ if (replacement === selectedLines)
5954
+ return false;
5955
+ textarea.setSelectionRange(lineStart, lineEnd);
5956
+ let inserted = false;
5957
+ try {
5958
+ inserted = document.execCommand("insertText", false, replacement);
5959
+ } catch (_) {
5960
+ }
5961
+ if (!inserted) {
5962
+ textarea.setRangeText(replacement, lineStart, lineEnd, "preserve");
5963
+ }
5964
+ textarea.setSelectionRange(lineStart, lineStart + replacement.length);
5965
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
5966
+ return true;
5967
+ }
5968
+ /**
5969
+ * @private
5970
+ */
5971
+ _effectiveSelectionEnd(value, selectionStart, selectionEnd) {
5972
+ if (selectionEnd > selectionStart && value[selectionEnd - 1] === "\n") {
5973
+ return selectionEnd - 1;
5974
+ }
5975
+ return selectionEnd;
5976
+ }
5977
+ /**
5978
+ * @private
5979
+ */
5980
+ _canEditTextarea() {
5981
+ return this.textarea && !this.textarea.disabled && !this.textarea.readOnly;
5982
+ }
5650
5983
  /**
5651
5984
  * Get clean HTML without any OverType-specific markup
5652
5985
  * Useful for exporting to other formats or storage
@@ -5870,6 +6203,7 @@ var _OverType = class _OverType {
5870
6203
  */
5871
6204
  showNormalEditMode() {
5872
6205
  this.container.dataset.mode = "normal";
6206
+ this._syncPreviewInteractivity();
5873
6207
  this.updatePreview();
5874
6208
  this._updateAutoHeight();
5875
6209
  requestAnimationFrame(() => {
@@ -5884,6 +6218,7 @@ var _OverType = class _OverType {
5884
6218
  */
5885
6219
  showPlainTextarea() {
5886
6220
  this.container.dataset.mode = "plain";
6221
+ this._syncPreviewInteractivity();
5887
6222
  this._updateAutoHeight();
5888
6223
  if (this.toolbar) {
5889
6224
  const toggleBtn = this.container.querySelector('[data-action="toggle-plain"]');
@@ -5900,6 +6235,7 @@ var _OverType = class _OverType {
5900
6235
  */
5901
6236
  showPreviewMode() {
5902
6237
  this.container.dataset.mode = "preview";
6238
+ this._syncPreviewInteractivity();
5903
6239
  this.updatePreview();
5904
6240
  this._updateAutoHeight();
5905
6241
  return this;
@@ -5918,6 +6254,10 @@ var _OverType = class _OverType {
5918
6254
  if (this.shortcuts) {
5919
6255
  this.shortcuts.destroy();
5920
6256
  }
6257
+ if (this._safariReflowRaf) {
6258
+ cancelAnimationFrame(this._safariReflowRaf);
6259
+ this._safariReflowRaf = null;
6260
+ }
5921
6261
  if (this.wrapper) {
5922
6262
  const content = this.getValue();
5923
6263
  this.wrapper.remove();
@@ -5949,11 +6289,16 @@ var _OverType = class _OverType {
5949
6289
  return elements.map((el) => {
5950
6290
  const options = { ...defaults };
5951
6291
  for (const attr of el.attributes) {
5952
- if (attr.name.startsWith("data-ot-")) {
5953
- const kebab = attr.name.slice(8);
5954
- const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
5955
- options[key] = _OverType._parseDataValue(attr.value);
6292
+ if (!attr.name.startsWith("data-ot-"))
6293
+ continue;
6294
+ const kebab = attr.name.slice(8);
6295
+ if (kebab.startsWith("textarea-") && kebab !== "textarea-props") {
6296
+ const propKey = kebab.slice(9).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6297
+ options.textareaProps = { ...options.textareaProps || {}, [propKey]: _OverType._parseDataValue(attr.value) };
6298
+ continue;
5956
6299
  }
6300
+ const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6301
+ options[key] = _OverType._parseDataValue(attr.value);
5957
6302
  }
5958
6303
  return new _OverType(el, options)[0];
5959
6304
  });
@@ -5998,6 +6343,14 @@ var _OverType = class _OverType {
5998
6343
  return null;
5999
6344
  if (value !== "" && !isNaN(Number(value)))
6000
6345
  return Number(value);
6346
+ const trimmed = value.trim();
6347
+ if (trimmed[0] === "{" || trimmed[0] === "[") {
6348
+ try {
6349
+ return JSON.parse(trimmed);
6350
+ } catch (e) {
6351
+ return value;
6352
+ }
6353
+ }
6001
6354
  return value;
6002
6355
  }
6003
6356
  /**
@@ -6176,8 +6529,13 @@ var _OverType = class _OverType {
6176
6529
  * Initialize global event listeners
6177
6530
  */
6178
6531
  static initGlobalListeners() {
6179
- if (_OverType.globalListenersInitialized)
6532
+ const globalScope = typeof window !== "undefined" ? window : globalThis;
6533
+ const globalListenersKey = "__overtypeGlobalListenersInitialized";
6534
+ if (_OverType.globalListenersInitialized || globalScope[globalListenersKey]) {
6535
+ _OverType.globalListenersInitialized = true;
6180
6536
  return;
6537
+ }
6538
+ globalScope[globalListenersKey] = true;
6181
6539
  document.addEventListener("input", (e) => {
6182
6540
  if (e.target && e.target.classList && e.target.classList.contains("overtype-input")) {
6183
6541
  const wrapper = e.target.closest(".overtype-wrapper");