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/README.md +171 -15
- package/dist/overtype-webcomponent.esm.js +510 -98
- package/dist/overtype-webcomponent.esm.js.map +3 -3
- package/dist/overtype-webcomponent.js +510 -98
- package/dist/overtype-webcomponent.js.map +3 -3
- package/dist/overtype-webcomponent.min.js +57 -56
- package/dist/overtype.cjs +510 -98
- package/dist/overtype.cjs.map +3 -3
- package/dist/overtype.d.ts +13 -0
- package/dist/overtype.esm.js +510 -98
- package/dist/overtype.esm.js.map +3 -3
- package/dist/overtype.js +510 -98
- package/dist/overtype.js.map +3 -3
- package/dist/overtype.min.js +53 -52
- package/package.json +3 -3
- package/src/link-tooltip.js +18 -2
- package/src/overtype.d.ts +13 -0
- package/src/overtype.js +249 -72
- package/src/shortcuts.js +12 -0
- package/src/toolbar-buttons.js +10 -0
- package/src/toolbar.js +308 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "overtype",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "A lightweight markdown editor library with perfect WYSIWYG alignment using an invisible textarea overlay",
|
|
5
5
|
"main": "dist/overtype.cjs",
|
|
6
6
|
"module": "dist/overtype.esm.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"types": "./dist/overtype.d.ts",
|
|
14
14
|
"import": "./dist/overtype.esm.js",
|
|
15
15
|
"require": "./dist/overtype.cjs",
|
|
16
|
-
"browser": "./dist/overtype.
|
|
16
|
+
"browser": "./dist/overtype.min.js"
|
|
17
17
|
},
|
|
18
18
|
"./parser": {
|
|
19
19
|
"import": "./src/parser.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"build:prod": "npm test && npm run build",
|
|
32
32
|
"dev": "http-server website -p 8080 -c-1",
|
|
33
33
|
"watch": "node scripts/build.js --watch",
|
|
34
|
-
"test": "node test/overtype.test.js && node test/preview-mode.test.js && node test/links.test.js && node test/api-methods.test.js && node test/comprehensive-alignment.test.js && node test/sanctuary-parsing.test.js && node test/mode-switching.test.js && node test/syntax-highlighting.test.js && node test/webcomponent.test.js && node test/custom-syntax.test.js && node test/auto-theme.test.js && npm run test:types",
|
|
34
|
+
"test": "node test/overtype.test.js && node test/preview-mode.test.js && node test/links.test.js && node test/api-methods.test.js && node test/keyboard-accessibility.test.js && node test/comprehensive-alignment.test.js && node test/sanctuary-parsing.test.js && node test/mode-switching.test.js && node test/toolbar.test.js && node test/syntax-highlighting.test.js && node test/webcomponent.test.js && node test/custom-syntax.test.js && node test/auto-theme.test.js && npm run test:types",
|
|
35
35
|
"test:main": "node test/overtype.test.js",
|
|
36
36
|
"test:preview": "node test/preview-mode.test.js",
|
|
37
37
|
"test:links": "node test/links.test.js",
|
package/src/link-tooltip.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { computePosition, offset, shift, flip } from '@floating-ui/dom';
|
|
7
|
+
import { MarkdownParser } from './parser.js';
|
|
7
8
|
|
|
8
9
|
export class LinkTooltip {
|
|
9
10
|
constructor(editor) {
|
|
@@ -85,7 +86,10 @@ export class LinkTooltip {
|
|
|
85
86
|
e.preventDefault();
|
|
86
87
|
e.stopPropagation();
|
|
87
88
|
if (this.currentLink) {
|
|
88
|
-
|
|
89
|
+
const safeUrl = MarkdownParser.sanitizeUrl(this.currentLink.url);
|
|
90
|
+
if (safeUrl !== '#') {
|
|
91
|
+
window.open(safeUrl, '_blank');
|
|
92
|
+
}
|
|
89
93
|
this.hide();
|
|
90
94
|
}
|
|
91
95
|
});
|
|
@@ -123,7 +127,7 @@ export class LinkTooltip {
|
|
|
123
127
|
if (position >= start && position <= end) {
|
|
124
128
|
return {
|
|
125
129
|
text: match[1],
|
|
126
|
-
url: match[2],
|
|
130
|
+
url: this.transformUrl(match[2]),
|
|
127
131
|
index: linkIndex,
|
|
128
132
|
start: start,
|
|
129
133
|
end: end
|
|
@@ -135,6 +139,18 @@ export class LinkTooltip {
|
|
|
135
139
|
return null;
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
transformUrl(url) {
|
|
143
|
+
const transform = this.editor.options.transformLinkUrl;
|
|
144
|
+
if (typeof transform !== 'function') return url;
|
|
145
|
+
try {
|
|
146
|
+
const result = transform(url);
|
|
147
|
+
return typeof result === 'string' ? result : url;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.warn('transformLinkUrl threw:', e);
|
|
150
|
+
return url;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
async show(linkInfo) {
|
|
139
155
|
this.currentLink = linkInfo;
|
|
140
156
|
this.cancelHide();
|
package/src/overtype.d.ts
CHANGED
|
@@ -84,6 +84,15 @@ export interface ToolbarButton {
|
|
|
84
84
|
setValue: (value: string) => void;
|
|
85
85
|
event: MouseEvent;
|
|
86
86
|
}) => void | Promise<void>;
|
|
87
|
+
|
|
88
|
+
/** Canonical action identifier used by shortcuts and performAction */
|
|
89
|
+
actionId?: string;
|
|
90
|
+
|
|
91
|
+
/** Return true when this button should be announced as pressed */
|
|
92
|
+
isActive?: (context: {
|
|
93
|
+
editor: OverType;
|
|
94
|
+
activeFormats: string[];
|
|
95
|
+
}) => boolean;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
export interface MobileOptions {
|
|
@@ -122,6 +131,7 @@ export interface Options {
|
|
|
122
131
|
spellcheck?: boolean; // Browser spellcheck (default: false)
|
|
123
132
|
statsFormatter?: (stats: Stats) => string;
|
|
124
133
|
codeHighlighter?: ((code: string, language: string) => string) | null; // Per-instance code highlighter
|
|
134
|
+
transformLinkUrl?: ((url: string) => string) | null; // Transform URLs shown/opened in the link tooltip
|
|
125
135
|
|
|
126
136
|
// Theme (deprecated in favor of global theme)
|
|
127
137
|
theme?: string | Theme;
|
|
@@ -135,6 +145,7 @@ export interface Options {
|
|
|
135
145
|
mimeTypes?: string[];
|
|
136
146
|
batch?: boolean;
|
|
137
147
|
onInsertFile: (file: File | File[]) => Promise<string | string[]>;
|
|
148
|
+
onRemoveFile?: (info: { url: string; filename: string; file: File }) => void;
|
|
138
149
|
};
|
|
139
150
|
|
|
140
151
|
// Callbacks
|
|
@@ -207,6 +218,8 @@ export interface OverTypeInstance {
|
|
|
207
218
|
showToolbar(): void;
|
|
208
219
|
hideToolbar(): void;
|
|
209
220
|
insertAtCursor(text: string): void;
|
|
221
|
+
indentSelection(): void;
|
|
222
|
+
outdentSelection(): void;
|
|
210
223
|
|
|
211
224
|
// HTML output methods
|
|
212
225
|
getRenderedHTML(options?: RenderOptions): string;
|
package/src/overtype.js
CHANGED
|
@@ -12,6 +12,25 @@ import { Toolbar } from './toolbar.js';
|
|
|
12
12
|
import { LinkTooltip } from './link-tooltip.js';
|
|
13
13
|
import { defaultToolbarButtons, toolbarButtons as builtinToolbarButtons } from './toolbar-buttons.js';
|
|
14
14
|
|
|
15
|
+
let _isSafariCache;
|
|
16
|
+
/**
|
|
17
|
+
* Detect Safari (desktop, iOS, and iPadOS), excluding Chromium/Firefox-on-iOS.
|
|
18
|
+
* Memoized; guards against non-browser environments.
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isSafariBrowser() {
|
|
22
|
+
if (_isSafariCache !== undefined) return _isSafariCache;
|
|
23
|
+
_isSafariCache = false;
|
|
24
|
+
if (typeof navigator !== 'undefined') {
|
|
25
|
+
const ua = navigator.userAgent || '';
|
|
26
|
+
_isSafariCache =
|
|
27
|
+
/^((?!chrome|android|crios|fxios|edg|opr).)*safari/i.test(ua) ||
|
|
28
|
+
/iPad|iPhone|iPod/.test(ua) ||
|
|
29
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 && /Safari/.test(ua));
|
|
30
|
+
}
|
|
31
|
+
return _isSafariCache;
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* Build action map from toolbar button configurations
|
|
17
36
|
* @param {Array} buttons - Array of button config objects
|
|
@@ -131,6 +150,8 @@ class OverType {
|
|
|
131
150
|
this.options = this._mergeOptions(options);
|
|
132
151
|
this.instanceId = ++OverType.instanceCount;
|
|
133
152
|
this.initialized = false;
|
|
153
|
+
this._isSafari = isSafariBrowser();
|
|
154
|
+
this._safariReflowRaf = null;
|
|
134
155
|
|
|
135
156
|
// Inject styles if needed
|
|
136
157
|
OverType.injectStyles();
|
|
@@ -225,7 +246,8 @@ class OverType {
|
|
|
225
246
|
statsFormatter: null,
|
|
226
247
|
smartLists: true, // Enable smart list continuation
|
|
227
248
|
codeHighlighter: null, // Per-instance code highlighter
|
|
228
|
-
spellcheck: false // Browser spellcheck (disabled by default)
|
|
249
|
+
spellcheck: false, // Browser spellcheck (disabled by default)
|
|
250
|
+
transformLinkUrl: null // Transform URLs shown/opened in the link tooltip
|
|
229
251
|
};
|
|
230
252
|
|
|
231
253
|
// Remove theme and colors from options - these are now global
|
|
@@ -296,6 +318,9 @@ class OverType {
|
|
|
296
318
|
|
|
297
319
|
// Disable autofill, spellcheck, and extensions
|
|
298
320
|
this._configureTextarea();
|
|
321
|
+
this._ensureTextareaId();
|
|
322
|
+
|
|
323
|
+
this._syncPreviewInteractivity();
|
|
299
324
|
|
|
300
325
|
// Apply any new options
|
|
301
326
|
this._applyOptions();
|
|
@@ -390,6 +415,8 @@ class OverType {
|
|
|
390
415
|
});
|
|
391
416
|
}
|
|
392
417
|
|
|
418
|
+
this._ensureTextareaId();
|
|
419
|
+
|
|
393
420
|
// Create preview div
|
|
394
421
|
this.preview = document.createElement('div');
|
|
395
422
|
this.preview.className = 'overtype-preview';
|
|
@@ -429,6 +456,8 @@ class OverType {
|
|
|
429
456
|
// Ensure auto-resize class is removed if not using auto-resize
|
|
430
457
|
this.container.classList.remove('overtype-auto-resize');
|
|
431
458
|
}
|
|
459
|
+
|
|
460
|
+
this._syncPreviewInteractivity();
|
|
432
461
|
}
|
|
433
462
|
|
|
434
463
|
/**
|
|
@@ -445,6 +474,35 @@ class OverType {
|
|
|
445
474
|
this.textarea.setAttribute('data-enable-grammarly', 'false');
|
|
446
475
|
}
|
|
447
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Ensure the textarea can be referenced by aria-controls
|
|
479
|
+
* @private
|
|
480
|
+
*/
|
|
481
|
+
_ensureTextareaId() {
|
|
482
|
+
if (!this.textarea.id) {
|
|
483
|
+
this.textarea.id = `overtype-${this.instanceId}-input`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Keep rendered preview content out of keyboard navigation until Preview mode.
|
|
489
|
+
* @private
|
|
490
|
+
*/
|
|
491
|
+
_syncPreviewInteractivity() {
|
|
492
|
+
if (!this.preview || !this.container) return;
|
|
493
|
+
|
|
494
|
+
const isPreviewMode = this.container.dataset.mode === 'preview';
|
|
495
|
+
this.preview.inert = !isPreviewMode;
|
|
496
|
+
this.preview.toggleAttribute('inert', !isPreviewMode);
|
|
497
|
+
|
|
498
|
+
if (isPreviewMode) {
|
|
499
|
+
this.preview.removeAttribute('aria-hidden');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
this.preview.setAttribute('aria-hidden', 'true');
|
|
504
|
+
}
|
|
505
|
+
|
|
448
506
|
/**
|
|
449
507
|
* Create and setup toolbar
|
|
450
508
|
* @private
|
|
@@ -605,6 +663,7 @@ class OverType {
|
|
|
605
663
|
}
|
|
606
664
|
|
|
607
665
|
this._fileUploadCounter = 0;
|
|
666
|
+
this._uploadedFiles = new Map(); // url -> { filename, file }
|
|
608
667
|
this._boundHandleFilePaste = this._handleFilePaste.bind(this);
|
|
609
668
|
this._boundHandleFileDrop = this._handleFileDrop.bind(this);
|
|
610
669
|
this._boundHandleDragOver = this._handleDragOver.bind(this);
|
|
@@ -616,6 +675,50 @@ class OverType {
|
|
|
616
675
|
this.fileUploadInitialized = true;
|
|
617
676
|
}
|
|
618
677
|
|
|
678
|
+
/**
|
|
679
|
+
* Extract URLs from markdown link syntax: [text](url) or .
|
|
680
|
+
* @private
|
|
681
|
+
*/
|
|
682
|
+
_extractMarkdownUrls(text) {
|
|
683
|
+
const urls = [];
|
|
684
|
+
const re = /!?\[[^\]]*\]\(([^)\s]+)/g;
|
|
685
|
+
let m;
|
|
686
|
+
while ((m = re.exec(text)) !== null) urls.push(m[1]);
|
|
687
|
+
return urls;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Track URLs that were just inserted, pairing each with the source File.
|
|
692
|
+
* If multiple URLs appear in one inserted block, all get associated with
|
|
693
|
+
* the same file (rare; happens if onInsertFile returns several links).
|
|
694
|
+
* @private
|
|
695
|
+
*/
|
|
696
|
+
_trackInsertedUrls(insertedText, file) {
|
|
697
|
+
if (!this._uploadedFiles || !file || !insertedText) return;
|
|
698
|
+
for (const url of this._extractMarkdownUrls(insertedText)) {
|
|
699
|
+
this._uploadedFiles.set(url, { filename: file.name, file });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Diff the tracked-URL set against the current value and fire
|
|
705
|
+
* fileUpload.onRemoveFile for any URL no longer present.
|
|
706
|
+
* @private
|
|
707
|
+
*/
|
|
708
|
+
_checkForRemovedUploads() {
|
|
709
|
+
if (!this._uploadedFiles || this._uploadedFiles.size === 0) return;
|
|
710
|
+
const cb = this.options.fileUpload?.onRemoveFile;
|
|
711
|
+
const value = this.textarea.value;
|
|
712
|
+
const removed = [];
|
|
713
|
+
for (const [url, info] of this._uploadedFiles) {
|
|
714
|
+
if (!value.includes(url)) removed.push({ url, info });
|
|
715
|
+
}
|
|
716
|
+
for (const { url, info } of removed) {
|
|
717
|
+
this._uploadedFiles.delete(url);
|
|
718
|
+
if (cb) cb({ url, filename: info.filename, file: info.file });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
619
722
|
_handleFilePaste(e) {
|
|
620
723
|
if (!e?.clipboardData?.files?.length) return;
|
|
621
724
|
e.preventDefault();
|
|
@@ -647,6 +750,7 @@ class OverType {
|
|
|
647
750
|
|
|
648
751
|
this.options.fileUpload.onInsertFile(file).then((text) => {
|
|
649
752
|
this.textarea.value = this.textarea.value.replace(placeholder, text);
|
|
753
|
+
this._trackInsertedUrls(text, file);
|
|
650
754
|
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
651
755
|
}, (error) => {
|
|
652
756
|
console.error('OverType: File upload failed', error);
|
|
@@ -660,6 +764,7 @@ class OverType {
|
|
|
660
764
|
const texts = Array.isArray(result) ? result : [result];
|
|
661
765
|
texts.forEach((text, index) => {
|
|
662
766
|
this.textarea.value = this.textarea.value.replace(files[index].placeholder, text);
|
|
767
|
+
this._trackInsertedUrls(text, files[index].file);
|
|
663
768
|
});
|
|
664
769
|
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
665
770
|
}, (error) => {
|
|
@@ -683,6 +788,7 @@ class OverType {
|
|
|
683
788
|
this._boundHandleFilePaste = null;
|
|
684
789
|
this._boundHandleFileDrop = null;
|
|
685
790
|
this._boundHandleDragOver = null;
|
|
791
|
+
this._uploadedFiles = null;
|
|
686
792
|
this.fileUploadInitialized = false;
|
|
687
793
|
}
|
|
688
794
|
|
|
@@ -746,8 +852,11 @@ class OverType {
|
|
|
746
852
|
* @private
|
|
747
853
|
*/
|
|
748
854
|
_notifyChange() {
|
|
749
|
-
if (!this.
|
|
750
|
-
this.
|
|
855
|
+
if (!this.initialized) return;
|
|
856
|
+
this._checkForRemovedUploads();
|
|
857
|
+
if (this.options.onChange) {
|
|
858
|
+
this.options.onChange(this.textarea.value, this);
|
|
859
|
+
}
|
|
751
860
|
}
|
|
752
861
|
|
|
753
862
|
/**
|
|
@@ -799,6 +908,28 @@ class OverType {
|
|
|
799
908
|
handleInput(event) {
|
|
800
909
|
this.updatePreview();
|
|
801
910
|
this._notifyChange();
|
|
911
|
+
this._scheduleSafariReflow();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Force Safari to re-shape stale textarea text after an edit.
|
|
916
|
+
* Safari can leave a textarea's glyph layout cached after incremental edits,
|
|
917
|
+
* desyncing the caret/wrap from the styled preview overlay. Toggling
|
|
918
|
+
* letter-spacing (with !important to beat the stylesheet rule) and reading
|
|
919
|
+
* offsetHeight forces a synchronous re-shape. Safari-only, coalesced to one
|
|
920
|
+
* run per animation frame.
|
|
921
|
+
* @private
|
|
922
|
+
*/
|
|
923
|
+
_scheduleSafariReflow() {
|
|
924
|
+
if (!this._isSafari || this._safariReflowRaf) return;
|
|
925
|
+
this._safariReflowRaf = requestAnimationFrame(() => {
|
|
926
|
+
this._safariReflowRaf = null;
|
|
927
|
+
const ta = this.textarea;
|
|
928
|
+
if (!ta) return;
|
|
929
|
+
ta.style.setProperty('letter-spacing', '-0.001px', 'important');
|
|
930
|
+
void ta.offsetHeight;
|
|
931
|
+
ta.style.removeProperty('letter-spacing');
|
|
932
|
+
});
|
|
802
933
|
}
|
|
803
934
|
|
|
804
935
|
/**
|
|
@@ -826,75 +957,16 @@ class OverType {
|
|
|
826
957
|
* @private
|
|
827
958
|
*/
|
|
828
959
|
handleKeydown(event) {
|
|
829
|
-
//
|
|
960
|
+
// Let collapsed Tab/Shift+Tab use native focus traversal.
|
|
830
961
|
if (event.key === 'Tab') {
|
|
831
962
|
const start = this.textarea.selectionStart;
|
|
832
963
|
const end = this.textarea.selectionEnd;
|
|
833
|
-
const value = this.textarea.value;
|
|
834
964
|
|
|
835
|
-
|
|
836
|
-
|
|
965
|
+
if (start !== end && this._canEditTextarea()) {
|
|
966
|
+
event.preventDefault();
|
|
967
|
+
event.shiftKey ? this.outdentSelection() : this.indentSelection();
|
|
837
968
|
return;
|
|
838
969
|
}
|
|
839
|
-
|
|
840
|
-
event.preventDefault();
|
|
841
|
-
|
|
842
|
-
// If there's a selection, indent/outdent based on shift key
|
|
843
|
-
if (start !== end && event.shiftKey) {
|
|
844
|
-
// Outdent: remove 2 spaces from start of each selected line
|
|
845
|
-
const before = value.substring(0, start);
|
|
846
|
-
const selection = value.substring(start, end);
|
|
847
|
-
const after = value.substring(end);
|
|
848
|
-
|
|
849
|
-
const lines = selection.split('\n');
|
|
850
|
-
const outdented = lines.map(line => line.replace(/^ /, '')).join('\n');
|
|
851
|
-
|
|
852
|
-
// Try to use execCommand first to preserve undo history
|
|
853
|
-
if (document.execCommand) {
|
|
854
|
-
// Select the text that needs to be replaced
|
|
855
|
-
this.textarea.setSelectionRange(start, end);
|
|
856
|
-
document.execCommand('insertText', false, outdented);
|
|
857
|
-
} else {
|
|
858
|
-
// Fallback to direct manipulation
|
|
859
|
-
this.textarea.value = before + outdented + after;
|
|
860
|
-
this.textarea.selectionStart = start;
|
|
861
|
-
this.textarea.selectionEnd = start + outdented.length;
|
|
862
|
-
}
|
|
863
|
-
} else if (start !== end) {
|
|
864
|
-
// Indent: add 2 spaces to start of each selected line
|
|
865
|
-
const before = value.substring(0, start);
|
|
866
|
-
const selection = value.substring(start, end);
|
|
867
|
-
const after = value.substring(end);
|
|
868
|
-
|
|
869
|
-
const lines = selection.split('\n');
|
|
870
|
-
const indented = lines.map(line => ' ' + line).join('\n');
|
|
871
|
-
|
|
872
|
-
// Try to use execCommand first to preserve undo history
|
|
873
|
-
if (document.execCommand) {
|
|
874
|
-
// Select the text that needs to be replaced
|
|
875
|
-
this.textarea.setSelectionRange(start, end);
|
|
876
|
-
document.execCommand('insertText', false, indented);
|
|
877
|
-
} else {
|
|
878
|
-
// Fallback to direct manipulation
|
|
879
|
-
this.textarea.value = before + indented + after;
|
|
880
|
-
this.textarea.selectionStart = start;
|
|
881
|
-
this.textarea.selectionEnd = start + indented.length;
|
|
882
|
-
}
|
|
883
|
-
} else {
|
|
884
|
-
// No selection: just insert 2 spaces
|
|
885
|
-
// Use execCommand to preserve undo history
|
|
886
|
-
if (document.execCommand) {
|
|
887
|
-
document.execCommand('insertText', false, ' ');
|
|
888
|
-
} else {
|
|
889
|
-
// Fallback to direct manipulation
|
|
890
|
-
this.textarea.value = value.substring(0, start) + ' ' + value.substring(end);
|
|
891
|
-
this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Trigger input event to update preview
|
|
896
|
-
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
897
|
-
return;
|
|
898
970
|
}
|
|
899
971
|
|
|
900
972
|
// Handle Enter key for smart list continuation
|
|
@@ -1083,6 +1155,7 @@ class OverType {
|
|
|
1083
1155
|
|
|
1084
1156
|
if (didChange) {
|
|
1085
1157
|
this._notifyChange();
|
|
1158
|
+
this._scheduleSafariReflow();
|
|
1086
1159
|
}
|
|
1087
1160
|
}
|
|
1088
1161
|
|
|
@@ -1154,6 +1227,77 @@ class OverType {
|
|
|
1154
1227
|
getPreviewHTML() {
|
|
1155
1228
|
return this.preview.innerHTML;
|
|
1156
1229
|
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Indent the current line or selected lines by two spaces.
|
|
1233
|
+
*/
|
|
1234
|
+
indentSelection() {
|
|
1235
|
+
this._replaceSelectedLines(line => ` ${line}`);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Outdent the current line or selected lines by up to two spaces or one tab.
|
|
1240
|
+
*/
|
|
1241
|
+
outdentSelection() {
|
|
1242
|
+
this._replaceSelectedLines(line => line.replace(/^( {1,2}|\t)/, ''));
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Replace full lines touched by the current selection.
|
|
1247
|
+
* @private
|
|
1248
|
+
*/
|
|
1249
|
+
_replaceSelectedLines(transformLine) {
|
|
1250
|
+
if (!this._canEditTextarea()) return false;
|
|
1251
|
+
|
|
1252
|
+
const textarea = this.textarea;
|
|
1253
|
+
const { selectionStart, selectionEnd, value } = textarea;
|
|
1254
|
+
const lineStart = value.lastIndexOf('\n', selectionStart - 1) + 1;
|
|
1255
|
+
const effectiveEnd = this._effectiveSelectionEnd(value, selectionStart, selectionEnd);
|
|
1256
|
+
const lineEndOffset = value.indexOf('\n', effectiveEnd);
|
|
1257
|
+
const lineEnd = lineEndOffset === -1 ? value.length : lineEndOffset;
|
|
1258
|
+
const selectedLines = value.slice(lineStart, lineEnd);
|
|
1259
|
+
const replacement = selectedLines
|
|
1260
|
+
.split('\n')
|
|
1261
|
+
.map(transformLine)
|
|
1262
|
+
.join('\n');
|
|
1263
|
+
|
|
1264
|
+
if (replacement === selectedLines) return false;
|
|
1265
|
+
|
|
1266
|
+
// Replace via execCommand to preserve native undo history (matches
|
|
1267
|
+
// insertAtCursor); fall back to setRangeText where execCommand is unavailable.
|
|
1268
|
+
textarea.setSelectionRange(lineStart, lineEnd);
|
|
1269
|
+
let inserted = false;
|
|
1270
|
+
try {
|
|
1271
|
+
inserted = document.execCommand('insertText', false, replacement);
|
|
1272
|
+
} catch (_) {}
|
|
1273
|
+
|
|
1274
|
+
if (!inserted) {
|
|
1275
|
+
textarea.setRangeText(replacement, lineStart, lineEnd, 'preserve');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
textarea.setSelectionRange(lineStart, lineStart + replacement.length);
|
|
1279
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1280
|
+
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* @private
|
|
1286
|
+
*/
|
|
1287
|
+
_effectiveSelectionEnd(value, selectionStart, selectionEnd) {
|
|
1288
|
+
if (selectionEnd > selectionStart && value[selectionEnd - 1] === '\n') {
|
|
1289
|
+
return selectionEnd - 1;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return selectionEnd;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* @private
|
|
1297
|
+
*/
|
|
1298
|
+
_canEditTextarea() {
|
|
1299
|
+
return this.textarea && !this.textarea.disabled && !this.textarea.readOnly;
|
|
1300
|
+
}
|
|
1157
1301
|
|
|
1158
1302
|
/**
|
|
1159
1303
|
* Get clean HTML without any OverType-specific markup
|
|
@@ -1444,6 +1588,7 @@ class OverType {
|
|
|
1444
1588
|
*/
|
|
1445
1589
|
showNormalEditMode() {
|
|
1446
1590
|
this.container.dataset.mode = 'normal';
|
|
1591
|
+
this._syncPreviewInteractivity();
|
|
1447
1592
|
this.updatePreview(); // Re-render with normal mode (e.g., show syntax markers)
|
|
1448
1593
|
this._updateAutoHeight();
|
|
1449
1594
|
|
|
@@ -1462,6 +1607,7 @@ class OverType {
|
|
|
1462
1607
|
*/
|
|
1463
1608
|
showPlainTextarea() {
|
|
1464
1609
|
this.container.dataset.mode = 'plain';
|
|
1610
|
+
this._syncPreviewInteractivity();
|
|
1465
1611
|
this._updateAutoHeight();
|
|
1466
1612
|
|
|
1467
1613
|
// Update toolbar button if exists
|
|
@@ -1482,6 +1628,7 @@ class OverType {
|
|
|
1482
1628
|
*/
|
|
1483
1629
|
showPreviewMode() {
|
|
1484
1630
|
this.container.dataset.mode = 'preview';
|
|
1631
|
+
this._syncPreviewInteractivity();
|
|
1485
1632
|
this.updatePreview(); // Re-render with preview mode (e.g., checkboxes)
|
|
1486
1633
|
this._updateAutoHeight();
|
|
1487
1634
|
return this;
|
|
@@ -1507,11 +1654,17 @@ class OverType {
|
|
|
1507
1654
|
this.shortcuts.destroy();
|
|
1508
1655
|
}
|
|
1509
1656
|
|
|
1657
|
+
// Cancel any pending Safari reflow nudge
|
|
1658
|
+
if (this._safariReflowRaf) {
|
|
1659
|
+
cancelAnimationFrame(this._safariReflowRaf);
|
|
1660
|
+
this._safariReflowRaf = null;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1510
1663
|
// Remove DOM if created by us
|
|
1511
1664
|
if (this.wrapper) {
|
|
1512
1665
|
const content = this.getValue();
|
|
1513
1666
|
this.wrapper.remove();
|
|
1514
|
-
|
|
1667
|
+
|
|
1515
1668
|
// Restore original content
|
|
1516
1669
|
this.element.textContent = content;
|
|
1517
1670
|
}
|
|
@@ -1547,11 +1700,19 @@ class OverType {
|
|
|
1547
1700
|
|
|
1548
1701
|
// Parse data-ot-* attributes (kebab-case to camelCase)
|
|
1549
1702
|
for (const attr of el.attributes) {
|
|
1550
|
-
if (attr.name.startsWith('data-ot-'))
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1703
|
+
if (!attr.name.startsWith('data-ot-')) continue;
|
|
1704
|
+
const kebab = attr.name.slice(8); // Remove 'data-ot-'
|
|
1705
|
+
|
|
1706
|
+
// data-ot-textarea-<attr> maps onto textareaProps (e.g. data-ot-textarea-required).
|
|
1707
|
+
// data-ot-textarea-props is the whole-object JSON form, handled generically below.
|
|
1708
|
+
if (kebab.startsWith('textarea-') && kebab !== 'textarea-props') {
|
|
1709
|
+
const propKey = kebab.slice(9).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1710
|
+
options.textareaProps = { ...(options.textareaProps || {}), [propKey]: OverType._parseDataValue(attr.value) };
|
|
1711
|
+
continue;
|
|
1554
1712
|
}
|
|
1713
|
+
|
|
1714
|
+
const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1715
|
+
options[key] = OverType._parseDataValue(attr.value);
|
|
1555
1716
|
}
|
|
1556
1717
|
|
|
1557
1718
|
return new OverType(el, options)[0];
|
|
@@ -1596,6 +1757,14 @@ class OverType {
|
|
|
1596
1757
|
if (value === 'false') return false;
|
|
1597
1758
|
if (value === 'null') return null;
|
|
1598
1759
|
if (value !== '' && !isNaN(Number(value))) return Number(value);
|
|
1760
|
+
const trimmed = value.trim();
|
|
1761
|
+
if (trimmed[0] === '{' || trimmed[0] === '[') {
|
|
1762
|
+
try {
|
|
1763
|
+
return JSON.parse(trimmed);
|
|
1764
|
+
} catch (e) {
|
|
1765
|
+
return value;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1599
1768
|
return value;
|
|
1600
1769
|
}
|
|
1601
1770
|
|
|
@@ -1804,7 +1973,15 @@ class OverType {
|
|
|
1804
1973
|
* Initialize global event listeners
|
|
1805
1974
|
*/
|
|
1806
1975
|
static initGlobalListeners() {
|
|
1807
|
-
|
|
1976
|
+
const globalScope = typeof window !== 'undefined' ? window : globalThis;
|
|
1977
|
+
const globalListenersKey = '__overtypeGlobalListenersInitialized';
|
|
1978
|
+
|
|
1979
|
+
if (OverType.globalListenersInitialized || globalScope[globalListenersKey]) {
|
|
1980
|
+
OverType.globalListenersInitialized = true;
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
globalScope[globalListenersKey] = true;
|
|
1808
1985
|
|
|
1809
1986
|
// Input event
|
|
1810
1987
|
document.addEventListener('input', (e) => {
|
package/src/shortcuts.js
CHANGED
|
@@ -22,6 +22,18 @@ export class ShortcutsManager {
|
|
|
22
22
|
|
|
23
23
|
if (!modKey) return false;
|
|
24
24
|
|
|
25
|
+
if (event.key === ']') {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
this.editor.indentSelection();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (event.key === '[') {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
this.editor.outdentSelection();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
let actionId = null;
|
|
26
38
|
|
|
27
39
|
switch (event.key.toLowerCase()) {
|