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.
- package/README.md +103 -11
- package/dist/overtype-webcomponent.esm.js +454 -96
- package/dist/overtype-webcomponent.esm.js.map +3 -3
- package/dist/overtype-webcomponent.js +454 -96
- package/dist/overtype-webcomponent.js.map +3 -3
- package/dist/overtype-webcomponent.min.js +57 -56
- package/dist/overtype.cjs +454 -96
- package/dist/overtype.cjs.map +3 -3
- package/dist/overtype.d.ts +12 -0
- package/dist/overtype.esm.js +454 -96
- package/dist/overtype.esm.js.map +3 -3
- package/dist/overtype.js +454 -96
- 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 +12 -0
- package/src/overtype.js +196 -70
- 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;
|
|
@@ -208,6 +218,8 @@ export interface OverTypeInstance {
|
|
|
208
218
|
showToolbar(): void;
|
|
209
219
|
hideToolbar(): void;
|
|
210
220
|
insertAtCursor(text: string): void;
|
|
221
|
+
indentSelection(): void;
|
|
222
|
+
outdentSelection(): void;
|
|
211
223
|
|
|
212
224
|
// HTML output methods
|
|
213
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
|
|
@@ -850,6 +908,28 @@ class OverType {
|
|
|
850
908
|
handleInput(event) {
|
|
851
909
|
this.updatePreview();
|
|
852
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
|
+
});
|
|
853
933
|
}
|
|
854
934
|
|
|
855
935
|
/**
|
|
@@ -877,75 +957,16 @@ class OverType {
|
|
|
877
957
|
* @private
|
|
878
958
|
*/
|
|
879
959
|
handleKeydown(event) {
|
|
880
|
-
//
|
|
960
|
+
// Let collapsed Tab/Shift+Tab use native focus traversal.
|
|
881
961
|
if (event.key === 'Tab') {
|
|
882
962
|
const start = this.textarea.selectionStart;
|
|
883
963
|
const end = this.textarea.selectionEnd;
|
|
884
|
-
const value = this.textarea.value;
|
|
885
964
|
|
|
886
|
-
|
|
887
|
-
|
|
965
|
+
if (start !== end && this._canEditTextarea()) {
|
|
966
|
+
event.preventDefault();
|
|
967
|
+
event.shiftKey ? this.outdentSelection() : this.indentSelection();
|
|
888
968
|
return;
|
|
889
969
|
}
|
|
890
|
-
|
|
891
|
-
event.preventDefault();
|
|
892
|
-
|
|
893
|
-
// If there's a selection, indent/outdent based on shift key
|
|
894
|
-
if (start !== end && event.shiftKey) {
|
|
895
|
-
// Outdent: remove 2 spaces from start of each selected line
|
|
896
|
-
const before = value.substring(0, start);
|
|
897
|
-
const selection = value.substring(start, end);
|
|
898
|
-
const after = value.substring(end);
|
|
899
|
-
|
|
900
|
-
const lines = selection.split('\n');
|
|
901
|
-
const outdented = lines.map(line => line.replace(/^ /, '')).join('\n');
|
|
902
|
-
|
|
903
|
-
// Try to use execCommand first to preserve undo history
|
|
904
|
-
if (document.execCommand) {
|
|
905
|
-
// Select the text that needs to be replaced
|
|
906
|
-
this.textarea.setSelectionRange(start, end);
|
|
907
|
-
document.execCommand('insertText', false, outdented);
|
|
908
|
-
} else {
|
|
909
|
-
// Fallback to direct manipulation
|
|
910
|
-
this.textarea.value = before + outdented + after;
|
|
911
|
-
this.textarea.selectionStart = start;
|
|
912
|
-
this.textarea.selectionEnd = start + outdented.length;
|
|
913
|
-
}
|
|
914
|
-
} else if (start !== end) {
|
|
915
|
-
// Indent: add 2 spaces to start of each selected line
|
|
916
|
-
const before = value.substring(0, start);
|
|
917
|
-
const selection = value.substring(start, end);
|
|
918
|
-
const after = value.substring(end);
|
|
919
|
-
|
|
920
|
-
const lines = selection.split('\n');
|
|
921
|
-
const indented = lines.map(line => ' ' + line).join('\n');
|
|
922
|
-
|
|
923
|
-
// Try to use execCommand first to preserve undo history
|
|
924
|
-
if (document.execCommand) {
|
|
925
|
-
// Select the text that needs to be replaced
|
|
926
|
-
this.textarea.setSelectionRange(start, end);
|
|
927
|
-
document.execCommand('insertText', false, indented);
|
|
928
|
-
} else {
|
|
929
|
-
// Fallback to direct manipulation
|
|
930
|
-
this.textarea.value = before + indented + after;
|
|
931
|
-
this.textarea.selectionStart = start;
|
|
932
|
-
this.textarea.selectionEnd = start + indented.length;
|
|
933
|
-
}
|
|
934
|
-
} else {
|
|
935
|
-
// No selection: just insert 2 spaces
|
|
936
|
-
// Use execCommand to preserve undo history
|
|
937
|
-
if (document.execCommand) {
|
|
938
|
-
document.execCommand('insertText', false, ' ');
|
|
939
|
-
} else {
|
|
940
|
-
// Fallback to direct manipulation
|
|
941
|
-
this.textarea.value = value.substring(0, start) + ' ' + value.substring(end);
|
|
942
|
-
this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// Trigger input event to update preview
|
|
947
|
-
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
948
|
-
return;
|
|
949
970
|
}
|
|
950
971
|
|
|
951
972
|
// Handle Enter key for smart list continuation
|
|
@@ -1134,6 +1155,7 @@ class OverType {
|
|
|
1134
1155
|
|
|
1135
1156
|
if (didChange) {
|
|
1136
1157
|
this._notifyChange();
|
|
1158
|
+
this._scheduleSafariReflow();
|
|
1137
1159
|
}
|
|
1138
1160
|
}
|
|
1139
1161
|
|
|
@@ -1205,6 +1227,77 @@ class OverType {
|
|
|
1205
1227
|
getPreviewHTML() {
|
|
1206
1228
|
return this.preview.innerHTML;
|
|
1207
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
|
+
}
|
|
1208
1301
|
|
|
1209
1302
|
/**
|
|
1210
1303
|
* Get clean HTML without any OverType-specific markup
|
|
@@ -1495,6 +1588,7 @@ class OverType {
|
|
|
1495
1588
|
*/
|
|
1496
1589
|
showNormalEditMode() {
|
|
1497
1590
|
this.container.dataset.mode = 'normal';
|
|
1591
|
+
this._syncPreviewInteractivity();
|
|
1498
1592
|
this.updatePreview(); // Re-render with normal mode (e.g., show syntax markers)
|
|
1499
1593
|
this._updateAutoHeight();
|
|
1500
1594
|
|
|
@@ -1513,6 +1607,7 @@ class OverType {
|
|
|
1513
1607
|
*/
|
|
1514
1608
|
showPlainTextarea() {
|
|
1515
1609
|
this.container.dataset.mode = 'plain';
|
|
1610
|
+
this._syncPreviewInteractivity();
|
|
1516
1611
|
this._updateAutoHeight();
|
|
1517
1612
|
|
|
1518
1613
|
// Update toolbar button if exists
|
|
@@ -1533,6 +1628,7 @@ class OverType {
|
|
|
1533
1628
|
*/
|
|
1534
1629
|
showPreviewMode() {
|
|
1535
1630
|
this.container.dataset.mode = 'preview';
|
|
1631
|
+
this._syncPreviewInteractivity();
|
|
1536
1632
|
this.updatePreview(); // Re-render with preview mode (e.g., checkboxes)
|
|
1537
1633
|
this._updateAutoHeight();
|
|
1538
1634
|
return this;
|
|
@@ -1558,11 +1654,17 @@ class OverType {
|
|
|
1558
1654
|
this.shortcuts.destroy();
|
|
1559
1655
|
}
|
|
1560
1656
|
|
|
1657
|
+
// Cancel any pending Safari reflow nudge
|
|
1658
|
+
if (this._safariReflowRaf) {
|
|
1659
|
+
cancelAnimationFrame(this._safariReflowRaf);
|
|
1660
|
+
this._safariReflowRaf = null;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1561
1663
|
// Remove DOM if created by us
|
|
1562
1664
|
if (this.wrapper) {
|
|
1563
1665
|
const content = this.getValue();
|
|
1564
1666
|
this.wrapper.remove();
|
|
1565
|
-
|
|
1667
|
+
|
|
1566
1668
|
// Restore original content
|
|
1567
1669
|
this.element.textContent = content;
|
|
1568
1670
|
}
|
|
@@ -1598,11 +1700,19 @@ class OverType {
|
|
|
1598
1700
|
|
|
1599
1701
|
// Parse data-ot-* attributes (kebab-case to camelCase)
|
|
1600
1702
|
for (const attr of el.attributes) {
|
|
1601
|
-
if (attr.name.startsWith('data-ot-'))
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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;
|
|
1605
1712
|
}
|
|
1713
|
+
|
|
1714
|
+
const key = kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1715
|
+
options[key] = OverType._parseDataValue(attr.value);
|
|
1606
1716
|
}
|
|
1607
1717
|
|
|
1608
1718
|
return new OverType(el, options)[0];
|
|
@@ -1647,6 +1757,14 @@ class OverType {
|
|
|
1647
1757
|
if (value === 'false') return false;
|
|
1648
1758
|
if (value === 'null') return null;
|
|
1649
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
|
+
}
|
|
1650
1768
|
return value;
|
|
1651
1769
|
}
|
|
1652
1770
|
|
|
@@ -1855,7 +1973,15 @@ class OverType {
|
|
|
1855
1973
|
* Initialize global event listeners
|
|
1856
1974
|
*/
|
|
1857
1975
|
static initGlobalListeners() {
|
|
1858
|
-
|
|
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;
|
|
1859
1985
|
|
|
1860
1986
|
// Input event
|
|
1861
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()) {
|
package/src/toolbar-buttons.js
CHANGED
|
@@ -19,6 +19,7 @@ export const toolbarButtons = {
|
|
|
19
19
|
actionId: 'toggleBold',
|
|
20
20
|
icon: icons.boldIcon,
|
|
21
21
|
title: 'Bold (Ctrl+B)',
|
|
22
|
+
isActive: ({ activeFormats }) => activeFormats.includes('bold'),
|
|
22
23
|
action: ({ editor }) => {
|
|
23
24
|
markdownActions.toggleBold(editor.textarea);
|
|
24
25
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -30,6 +31,7 @@ export const toolbarButtons = {
|
|
|
30
31
|
actionId: 'toggleItalic',
|
|
31
32
|
icon: icons.italicIcon,
|
|
32
33
|
title: 'Italic (Ctrl+I)',
|
|
34
|
+
isActive: ({ activeFormats }) => activeFormats.includes('italic'),
|
|
33
35
|
action: ({ editor }) => {
|
|
34
36
|
markdownActions.toggleItalic(editor.textarea);
|
|
35
37
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -41,6 +43,7 @@ export const toolbarButtons = {
|
|
|
41
43
|
actionId: 'toggleCode',
|
|
42
44
|
icon: icons.codeIcon,
|
|
43
45
|
title: 'Inline Code',
|
|
46
|
+
isActive: () => false,
|
|
44
47
|
action: ({ editor }) => {
|
|
45
48
|
markdownActions.toggleCode(editor.textarea);
|
|
46
49
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -68,6 +71,7 @@ export const toolbarButtons = {
|
|
|
68
71
|
actionId: 'toggleH1',
|
|
69
72
|
icon: icons.h1Icon,
|
|
70
73
|
title: 'Heading 1',
|
|
74
|
+
isActive: ({ activeFormats }) => activeFormats.includes('header'),
|
|
71
75
|
action: ({ editor }) => {
|
|
72
76
|
markdownActions.toggleH1(editor.textarea);
|
|
73
77
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -79,6 +83,7 @@ export const toolbarButtons = {
|
|
|
79
83
|
actionId: 'toggleH2',
|
|
80
84
|
icon: icons.h2Icon,
|
|
81
85
|
title: 'Heading 2',
|
|
86
|
+
isActive: ({ activeFormats }) => activeFormats.includes('header-2'),
|
|
82
87
|
action: ({ editor }) => {
|
|
83
88
|
markdownActions.toggleH2(editor.textarea);
|
|
84
89
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -90,6 +95,7 @@ export const toolbarButtons = {
|
|
|
90
95
|
actionId: 'toggleH3',
|
|
91
96
|
icon: icons.h3Icon,
|
|
92
97
|
title: 'Heading 3',
|
|
98
|
+
isActive: ({ activeFormats }) => activeFormats.includes('header-3'),
|
|
93
99
|
action: ({ editor }) => {
|
|
94
100
|
markdownActions.toggleH3(editor.textarea);
|
|
95
101
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -101,6 +107,7 @@ export const toolbarButtons = {
|
|
|
101
107
|
actionId: 'toggleBulletList',
|
|
102
108
|
icon: icons.bulletListIcon,
|
|
103
109
|
title: 'Bullet List',
|
|
110
|
+
isActive: ({ activeFormats }) => activeFormats.includes('bullet-list'),
|
|
104
111
|
action: ({ editor }) => {
|
|
105
112
|
markdownActions.toggleBulletList(editor.textarea);
|
|
106
113
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -112,6 +119,7 @@ export const toolbarButtons = {
|
|
|
112
119
|
actionId: 'toggleNumberedList',
|
|
113
120
|
icon: icons.orderedListIcon,
|
|
114
121
|
title: 'Numbered List',
|
|
122
|
+
isActive: ({ activeFormats }) => activeFormats.includes('numbered-list'),
|
|
115
123
|
action: ({ editor }) => {
|
|
116
124
|
markdownActions.toggleNumberedList(editor.textarea);
|
|
117
125
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -123,6 +131,7 @@ export const toolbarButtons = {
|
|
|
123
131
|
actionId: 'toggleTaskList',
|
|
124
132
|
icon: icons.taskListIcon,
|
|
125
133
|
title: 'Task List',
|
|
134
|
+
isActive: ({ activeFormats }) => activeFormats.includes('task-list'),
|
|
126
135
|
action: ({ editor }) => {
|
|
127
136
|
if (markdownActions.toggleTaskList) {
|
|
128
137
|
markdownActions.toggleTaskList(editor.textarea);
|
|
@@ -136,6 +145,7 @@ export const toolbarButtons = {
|
|
|
136
145
|
actionId: 'toggleQuote',
|
|
137
146
|
icon: icons.quoteIcon,
|
|
138
147
|
title: 'Quote',
|
|
148
|
+
isActive: ({ activeFormats }) => activeFormats.includes('quote'),
|
|
139
149
|
action: ({ editor }) => {
|
|
140
150
|
markdownActions.toggleQuote(editor.textarea);
|
|
141
151
|
editor.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|